mirror of
				https://github.com/pure-admin/vue-pure-admin.git
				synced 2025-11-03 13:44:47 +08:00 
			
		
		
		
	
							parent
							
								
									bf128f183f
								
							
						
					
					
						commit
						f0a80c680e
					
				@ -55,6 +55,8 @@
 | 
			
		||||
    "@pureadmin/descriptions": "^1.2.1",
 | 
			
		||||
    "@pureadmin/table": "^3.1.2",
 | 
			
		||||
    "@pureadmin/utils": "^2.4.7",
 | 
			
		||||
    "@vue-flow/background": "^1.3.0",
 | 
			
		||||
    "@vue-flow/core": "^1.33.4",
 | 
			
		||||
    "@vueuse/core": "^10.9.0",
 | 
			
		||||
    "@vueuse/motion": "^2.1.0",
 | 
			
		||||
    "@wangeditor/editor": "^5.1.23",
 | 
			
		||||
@ -114,6 +116,7 @@
 | 
			
		||||
    "@iconify/vue": "^4.1.1",
 | 
			
		||||
    "@intlify/unplugin-vue-i18n": "^2.0.0",
 | 
			
		||||
    "@pureadmin/theme": "^3.2.0",
 | 
			
		||||
    "@types/dagre": "^0.7.52",
 | 
			
		||||
    "@types/gradient-string": "^1.1.5",
 | 
			
		||||
    "@types/intro.js": "^5.1.5",
 | 
			
		||||
    "@types/js-cookie": "^3.0.6",
 | 
			
		||||
@ -130,6 +133,7 @@
 | 
			
		||||
    "boxen": "^7.1.1",
 | 
			
		||||
    "cloc": "^2.11.0",
 | 
			
		||||
    "cssnano": "^6.1.0",
 | 
			
		||||
    "dagre": "^0.8.5",
 | 
			
		||||
    "eslint": "^8.57.0",
 | 
			
		||||
    "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
    "eslint-define-config": "^2.1.0",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										118
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										118
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -26,6 +26,12 @@ dependencies:
 | 
			
		||||
  '@pureadmin/utils':
 | 
			
		||||
    specifier: ^2.4.7
 | 
			
		||||
    version: 2.4.7(echarts@5.5.0)(vue@3.4.21)
 | 
			
		||||
  '@vue-flow/background':
 | 
			
		||||
    specifier: ^1.3.0
 | 
			
		||||
    version: 1.3.0(@vue-flow/core@1.33.4)(vue@3.4.21)
 | 
			
		||||
  '@vue-flow/core':
 | 
			
		||||
    specifier: ^1.33.4
 | 
			
		||||
    version: 1.33.4(vue@3.4.21)
 | 
			
		||||
  '@vueuse/core':
 | 
			
		||||
    specifier: ^10.9.0
 | 
			
		||||
    version: 10.9.0(vue@3.4.21)
 | 
			
		||||
@ -199,6 +205,9 @@ devDependencies:
 | 
			
		||||
  '@pureadmin/theme':
 | 
			
		||||
    specifier: ^3.2.0
 | 
			
		||||
    version: 3.2.0
 | 
			
		||||
  '@types/dagre':
 | 
			
		||||
    specifier: ^0.7.52
 | 
			
		||||
    version: 0.7.52
 | 
			
		||||
  '@types/gradient-string':
 | 
			
		||||
    specifier: ^1.1.5
 | 
			
		||||
    version: 1.1.5
 | 
			
		||||
@ -247,6 +256,9 @@ devDependencies:
 | 
			
		||||
  cssnano:
 | 
			
		||||
    specifier: ^6.1.0
 | 
			
		||||
    version: 6.1.0(postcss@8.4.35)
 | 
			
		||||
  dagre:
 | 
			
		||||
    specifier: ^0.8.5
 | 
			
		||||
    version: 0.8.5
 | 
			
		||||
  eslint:
 | 
			
		||||
    specifier: ^8.57.0
 | 
			
		||||
    version: 8.57.0
 | 
			
		||||
@ -2003,6 +2015,10 @@ packages:
 | 
			
		||||
      '@types/node': 20.11.27
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@types/dagre@0.7.52:
 | 
			
		||||
    resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@types/estree@1.0.5:
 | 
			
		||||
    resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
 | 
			
		||||
 | 
			
		||||
@ -2352,6 +2368,30 @@ packages:
 | 
			
		||||
      path-browserify: 1.0.1
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /@vue-flow/background@1.3.0(@vue-flow/core@1.33.4)(vue@3.4.21):
 | 
			
		||||
    resolution: {integrity: sha512-fu/8s9wzSOQIitnSTI10XT3bzTtagh4h8EF2SWwtlDklOZjAaKy75lqv4htHa3wigy/r4LGCOGwLw3Pk88/AxA==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      '@vue-flow/core': ^1.23.0
 | 
			
		||||
      vue: ^3.3.0
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@vue-flow/core': 1.33.4(vue@3.4.21)
 | 
			
		||||
      vue: 3.4.21(typescript@5.4.2)
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@vue-flow/core@1.33.4(vue@3.4.21):
 | 
			
		||||
    resolution: {integrity: sha512-ryoamKfQ5pgtdv//Gjpyc4nsawMOwfI2jVzOPvZ92VQs78L4lidiWD7UybqeEkrGw6UPue1CGlzoy/4KlOWcSg==}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      vue: ^3.3.0
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@vueuse/core': 10.9.0(vue@3.4.21)
 | 
			
		||||
      d3-drag: 3.0.0
 | 
			
		||||
      d3-selection: 3.0.0
 | 
			
		||||
      d3-zoom: 3.0.0
 | 
			
		||||
      vue: 3.4.21(typescript@5.4.2)
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - '@vue/composition-api'
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@vue/babel-helper-vue-transform-on@1.2.1:
 | 
			
		||||
    resolution: {integrity: sha512-jtEXim+pfyHWwvheYwUwSXm43KwQo8nhOBDyjrUITV6X2tB7lJm6n/+4sqR8137UVZZul5hBzWHdZ2uStYpyRQ==}
 | 
			
		||||
    dev: true
 | 
			
		||||
@ -3851,6 +3891,71 @@ packages:
 | 
			
		||||
  /csstype@3.1.3:
 | 
			
		||||
    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 | 
			
		||||
 | 
			
		||||
  /d3-color@3.1.0:
 | 
			
		||||
    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-dispatch@3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-drag@3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      d3-dispatch: 3.0.1
 | 
			
		||||
      d3-selection: 3.0.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-ease@3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-interpolate@3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      d3-color: 3.1.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-selection@3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-timer@3.0.1:
 | 
			
		||||
    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-transition@3.0.1(d3-selection@3.0.0):
 | 
			
		||||
    resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    peerDependencies:
 | 
			
		||||
      d3-selection: 2 - 3
 | 
			
		||||
    dependencies:
 | 
			
		||||
      d3-color: 3.1.0
 | 
			
		||||
      d3-dispatch: 3.0.1
 | 
			
		||||
      d3-ease: 3.0.1
 | 
			
		||||
      d3-interpolate: 3.0.1
 | 
			
		||||
      d3-selection: 3.0.0
 | 
			
		||||
      d3-timer: 3.0.1
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d3-zoom@3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
 | 
			
		||||
    engines: {node: '>=12'}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      d3-dispatch: 3.0.1
 | 
			
		||||
      d3-drag: 3.0.0
 | 
			
		||||
      d3-interpolate: 3.0.1
 | 
			
		||||
      d3-selection: 3.0.0
 | 
			
		||||
      d3-transition: 3.0.1(d3-selection@3.0.0)
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /d@1.0.2:
 | 
			
		||||
    resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
 | 
			
		||||
    engines: {node: '>=0.12'}
 | 
			
		||||
@ -3859,6 +3964,13 @@ packages:
 | 
			
		||||
      type: 2.7.2
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /dagre@0.8.5:
 | 
			
		||||
    resolution: {integrity: sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      graphlib: 2.1.8
 | 
			
		||||
      lodash: 4.17.21
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /danmu.js@1.1.13:
 | 
			
		||||
    resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
@ -4962,6 +5074,12 @@ packages:
 | 
			
		||||
    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /graphlib@2.1.8:
 | 
			
		||||
    resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      lodash: 4.17.21
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /has-flag@3.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
 | 
			
		||||
    engines: {node: '>=4'}
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,31 @@
 | 
			
		||||
// 完整版菜单比较多,将 rank 抽离出来,在此方便维护
 | 
			
		||||
 | 
			
		||||
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
 | 
			
		||||
  components = 1,
 | 
			
		||||
  able = 2,
 | 
			
		||||
  table = 3,
 | 
			
		||||
  list = 4,
 | 
			
		||||
  result = 5,
 | 
			
		||||
  error = 6,
 | 
			
		||||
  frame = 7,
 | 
			
		||||
  nested = 8,
 | 
			
		||||
  permission = 9,
 | 
			
		||||
  system = 10,
 | 
			
		||||
  monitor = 11,
 | 
			
		||||
  tabs = 12,
 | 
			
		||||
  about = 13,
 | 
			
		||||
  editor = 14,
 | 
			
		||||
  flowchart = 15,
 | 
			
		||||
  formdesign = 16,
 | 
			
		||||
  board = 17,
 | 
			
		||||
  ppt = 18,
 | 
			
		||||
  guide = 19,
 | 
			
		||||
  menuoverflow = 20;
 | 
			
		||||
  vueflow = 1,
 | 
			
		||||
  components = 2,
 | 
			
		||||
  able = 3,
 | 
			
		||||
  table = 4,
 | 
			
		||||
  list = 5,
 | 
			
		||||
  result = 6,
 | 
			
		||||
  error = 7,
 | 
			
		||||
  frame = 8,
 | 
			
		||||
  nested = 9,
 | 
			
		||||
  permission = 10,
 | 
			
		||||
  system = 11,
 | 
			
		||||
  monitor = 12,
 | 
			
		||||
  tabs = 13,
 | 
			
		||||
  about = 14,
 | 
			
		||||
  editor = 15,
 | 
			
		||||
  flowchart = 16,
 | 
			
		||||
  formdesign = 17,
 | 
			
		||||
  board = 18,
 | 
			
		||||
  ppt = 19,
 | 
			
		||||
  guide = 20,
 | 
			
		||||
  menuoverflow = 21;
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  home,
 | 
			
		||||
  vueflow,
 | 
			
		||||
  components,
 | 
			
		||||
  able,
 | 
			
		||||
  table,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								src/router/modules/vueflow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/router/modules/vueflow.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import { vueflow } from "@/router/enums";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  path: "/vue-flow",
 | 
			
		||||
  redirect: "/vue-flow/index",
 | 
			
		||||
  meta: {
 | 
			
		||||
    icon: "ep:set-up",
 | 
			
		||||
    title: "vue-flow",
 | 
			
		||||
    rank: vueflow
 | 
			
		||||
  },
 | 
			
		||||
  children: [
 | 
			
		||||
    {
 | 
			
		||||
      path: "/vue-flow/index",
 | 
			
		||||
      name: "VueFlow",
 | 
			
		||||
      component: () => import("@/views/vue-flow/layouting/index.vue"),
 | 
			
		||||
      meta: {
 | 
			
		||||
        title: "vue-flow",
 | 
			
		||||
        extraIcon: "IF-pure-iconfont-new svg"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
} satisfies RouteConfigsTable;
 | 
			
		||||
							
								
								
									
										214
									
								
								src/views/vue-flow/layouting/animationEdge.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/views/vue-flow/layouting/animationEdge.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,214 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, nextTick, ref, toRef, watch } from "vue";
 | 
			
		||||
import { TransitionPresets, executeTransition } from "@vueuse/core";
 | 
			
		||||
import {
 | 
			
		||||
  Position,
 | 
			
		||||
  BaseEdge,
 | 
			
		||||
  useVueFlow,
 | 
			
		||||
  useNodesData,
 | 
			
		||||
  getSmoothStepPath,
 | 
			
		||||
  EdgeLabelRenderer
 | 
			
		||||
} from "@vue-flow/core";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  source: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  target: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  sourceX: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  sourceY: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  targetX: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  targetY: {
 | 
			
		||||
    type: Number,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  sourcePosition: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: Position.Right
 | 
			
		||||
  },
 | 
			
		||||
  targetPosition: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    default: Position.Left
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { findEdge } = useVueFlow();
 | 
			
		||||
 | 
			
		||||
const nodesData = useNodesData([props.target, props.source]);
 | 
			
		||||
 | 
			
		||||
const edgePoint = ref(0);
 | 
			
		||||
 | 
			
		||||
const edgeRef = ref();
 | 
			
		||||
 | 
			
		||||
const labelPosition = ref({ x: 0, y: 0 });
 | 
			
		||||
 | 
			
		||||
const currentLength = ref(0);
 | 
			
		||||
 | 
			
		||||
const targetNodeData = toRef(() => nodesData.value[0].data);
 | 
			
		||||
 | 
			
		||||
const sourceNodeData = toRef(() => nodesData.value[1].data);
 | 
			
		||||
 | 
			
		||||
const isFinished = toRef(() => sourceNodeData.value.isFinished);
 | 
			
		||||
 | 
			
		||||
const isCancelled = toRef(() => targetNodeData.value.isCancelled);
 | 
			
		||||
 | 
			
		||||
const isAnimating = ref(false);
 | 
			
		||||
 | 
			
		||||
const edgeColor = toRef(() => {
 | 
			
		||||
  if (targetNodeData.value.hasError) {
 | 
			
		||||
    return "#f87171";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (targetNodeData.value.isFinished) {
 | 
			
		||||
    return "#42B983";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (targetNodeData.value.isCancelled || targetNodeData.value.isSkipped) {
 | 
			
		||||
    return "#fbbf24";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (targetNodeData.value.isRunning || isAnimating.value) {
 | 
			
		||||
    return "#2563eb";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return "#6b7280";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
const path = computed(() => getSmoothStepPath(props));
 | 
			
		||||
 | 
			
		||||
watch(isCancelled, isCancelled => {
 | 
			
		||||
  if (isCancelled) {
 | 
			
		||||
    reset();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(isAnimating, isAnimating => {
 | 
			
		||||
  const edge = findEdge(props.id);
 | 
			
		||||
 | 
			
		||||
  if (edge) {
 | 
			
		||||
    edge.data = {
 | 
			
		||||
      ...edge.data,
 | 
			
		||||
      isAnimating
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(edgePoint, point => {
 | 
			
		||||
  const pathEl = edgeRef.value?.pathEl;
 | 
			
		||||
 | 
			
		||||
  if (!pathEl || point === 0 || !isAnimating.value) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const nextLength = pathEl.getTotalLength();
 | 
			
		||||
 | 
			
		||||
  if (currentLength.value !== nextLength) {
 | 
			
		||||
    runAnimation();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  labelPosition.value = pathEl.getPointAtLength(point);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(isFinished, isFinished => {
 | 
			
		||||
  if (isFinished) {
 | 
			
		||||
    runAnimation();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function runAnimation() {
 | 
			
		||||
  const pathEl = edgeRef.value?.pathEl;
 | 
			
		||||
 | 
			
		||||
  if (!pathEl) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const totalLength = pathEl.getTotalLength();
 | 
			
		||||
 | 
			
		||||
  const from = edgePoint.value || 0;
 | 
			
		||||
 | 
			
		||||
  labelPosition.value = pathEl.getPointAtLength(from);
 | 
			
		||||
 | 
			
		||||
  isAnimating.value = true;
 | 
			
		||||
 | 
			
		||||
  if (currentLength.value !== totalLength) {
 | 
			
		||||
    currentLength.value = totalLength;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await executeTransition(edgePoint, from, totalLength, {
 | 
			
		||||
    transition: TransitionPresets.easeInOutCubic,
 | 
			
		||||
    duration: Math.max(1500, totalLength / 2),
 | 
			
		||||
    abort: () => !isAnimating.value
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reset() {
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    edgePoint.value = 0;
 | 
			
		||||
    currentLength.value = 0;
 | 
			
		||||
    labelPosition.value = { x: 0, y: 0 };
 | 
			
		||||
    isAnimating.value = false;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <BaseEdge
 | 
			
		||||
    :id="id"
 | 
			
		||||
    ref="edgeRef"
 | 
			
		||||
    :path="path[0]"
 | 
			
		||||
    :style="{ stroke: edgeColor }"
 | 
			
		||||
  />
 | 
			
		||||
 | 
			
		||||
  <EdgeLabelRenderer v-if="isAnimating">
 | 
			
		||||
    <div
 | 
			
		||||
      :style="{
 | 
			
		||||
        transform: `translate(-50%, -50%) translate(${labelPosition.x}px,${labelPosition.y}px)`
 | 
			
		||||
      }"
 | 
			
		||||
      class="nodrag nopan animated-edge-label"
 | 
			
		||||
    >
 | 
			
		||||
      <span class="truck">
 | 
			
		||||
        <span class="box">📦</span>
 | 
			
		||||
        🚚
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </EdgeLabelRenderer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.animated-edge-label {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.truck {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  transform: scaleX(-1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.box {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -10px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										85
									
								
								src/views/vue-flow/layouting/icon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/views/vue-flow/layouting/icon.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
defineProps({
 | 
			
		||||
  name: {
 | 
			
		||||
    type: String,
 | 
			
		||||
    required: true
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <svg
 | 
			
		||||
    v-if="name === 'play'"
 | 
			
		||||
    viewBox="0 0 24 24"
 | 
			
		||||
    height="24"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path d="M8 5v14l11-7z" fill="currentColor" />
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
  <svg
 | 
			
		||||
    v-else-if="name === 'stop'"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
    viewBox="0 0 24 24"
 | 
			
		||||
    height="24"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      fill="currentColor"
 | 
			
		||||
      d="M8 16h8V8H8zm4 6q-2.075 0-3.9-.788t-3.175-2.137q-1.35-1.35-2.137-3.175T2 12q0-2.075.788-3.9t2.137-3.175q1.35-1.35 3.175-2.137T12 2q2.075 0 3.9.788t3.175 2.137q1.35 1.35 2.138 3.175T22 12q0 2.075-.788 3.9t-2.137 3.175q-1.35 1.35-3.175 2.138T12 22"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
  <svg
 | 
			
		||||
    v-else-if="name === 'horizontal'"
 | 
			
		||||
    viewBox="0 0 24 24"
 | 
			
		||||
    height="24"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path d="M2,12 L22,12" stroke="currentColor" stroke-width="2" />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M7,7 L2,12 L7,17"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      stroke-width="2"
 | 
			
		||||
      fill="none"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M17,7 L22,12 L17,17"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      stroke-width="2"
 | 
			
		||||
      fill="none"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
  <svg
 | 
			
		||||
    v-else-if="name === 'vertical'"
 | 
			
		||||
    viewBox="0 0 24 24"
 | 
			
		||||
    height="24"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
  >
 | 
			
		||||
    <path d="M12,2 L12,22" stroke="currentColor" stroke-width="2" />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M7,7 L12,2 L17,7"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      stroke-width="2"
 | 
			
		||||
      fill="none"
 | 
			
		||||
    />
 | 
			
		||||
    <path
 | 
			
		||||
      d="M7,17 L12,22 L17,17"
 | 
			
		||||
      stroke="currentColor"
 | 
			
		||||
      stroke-width="2"
 | 
			
		||||
      fill="none"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
 | 
			
		||||
  <svg
 | 
			
		||||
    v-else-if="name === 'shuffle'"
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
    viewBox="0 0 24 24"
 | 
			
		||||
    height="24"
 | 
			
		||||
  >
 | 
			
		||||
    <path
 | 
			
		||||
      fill="currentColor"
 | 
			
		||||
      d="M14 20v-2h2.6l-3.175-3.175L14.85 13.4L18 16.55V14h2v6zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4zm3.775-9.425L4 5.4L5.4 4l5.175 5.175z"
 | 
			
		||||
    />
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
							
								
								
									
										214
									
								
								src/views/vue-flow/layouting/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/views/vue-flow/layouting/index.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,214 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import "@vue-flow/core/dist/style.css";
 | 
			
		||||
import "@vue-flow/core/dist/theme-default.css";
 | 
			
		||||
import Icon from "./icon.vue";
 | 
			
		||||
import { nextTick, ref } from "vue";
 | 
			
		||||
import { useLayout } from "./useLayout";
 | 
			
		||||
import { useShuffle } from "./useShuffle";
 | 
			
		||||
import ProcessNode from "./processNode.vue";
 | 
			
		||||
import { useRunProcess } from "./useRunProcess";
 | 
			
		||||
import AnimationEdge from "./animationEdge.vue";
 | 
			
		||||
import { Background } from "@vue-flow/background";
 | 
			
		||||
import { Panel, VueFlow, useVueFlow } from "@vue-flow/core";
 | 
			
		||||
import { initialEdges, initialNodes } from "./initialElements";
 | 
			
		||||
 | 
			
		||||
const nodes = ref(initialNodes);
 | 
			
		||||
 | 
			
		||||
const edges = ref(initialEdges);
 | 
			
		||||
 | 
			
		||||
const cancelOnError = ref(true);
 | 
			
		||||
 | 
			
		||||
const shuffle = useShuffle();
 | 
			
		||||
 | 
			
		||||
const { graph, layout, previousDirection } = useLayout();
 | 
			
		||||
 | 
			
		||||
// @ts-expect-error
 | 
			
		||||
const { run, stop, reset, isRunning } = useRunProcess({ graph, cancelOnError });
 | 
			
		||||
 | 
			
		||||
const { fitView } = useVueFlow();
 | 
			
		||||
 | 
			
		||||
async function shuffleGraph() {
 | 
			
		||||
  await stop();
 | 
			
		||||
 | 
			
		||||
  reset(nodes.value);
 | 
			
		||||
 | 
			
		||||
  edges.value = shuffle(nodes.value);
 | 
			
		||||
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    layoutGraph(previousDirection.value);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function layoutGraph(direction) {
 | 
			
		||||
  await stop();
 | 
			
		||||
 | 
			
		||||
  reset(nodes.value);
 | 
			
		||||
 | 
			
		||||
  nodes.value = layout(nodes.value, edges.value, direction);
 | 
			
		||||
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    fitView();
 | 
			
		||||
    run(nodes.value);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="layout-flow">
 | 
			
		||||
    <VueFlow
 | 
			
		||||
      :nodes="nodes"
 | 
			
		||||
      :edges="edges"
 | 
			
		||||
      @nodes-initialized="layoutGraph('LR')"
 | 
			
		||||
    >
 | 
			
		||||
      <template #node-process="props">
 | 
			
		||||
        <ProcessNode
 | 
			
		||||
          :data="props.data"
 | 
			
		||||
          :source-position="props.sourcePosition"
 | 
			
		||||
          :target-position="props.targetPosition"
 | 
			
		||||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <template #edge-animation="edgeProps">
 | 
			
		||||
        <AnimationEdge
 | 
			
		||||
          :id="edgeProps.id"
 | 
			
		||||
          :source="edgeProps.source"
 | 
			
		||||
          :target="edgeProps.target"
 | 
			
		||||
          :source-x="edgeProps.sourceX"
 | 
			
		||||
          :source-y="edgeProps.sourceY"
 | 
			
		||||
          :targetX="edgeProps.targetX"
 | 
			
		||||
          :targetY="edgeProps.targetY"
 | 
			
		||||
          :source-position="edgeProps.sourcePosition"
 | 
			
		||||
          :target-position="edgeProps.targetPosition"
 | 
			
		||||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
 | 
			
		||||
      <Background />
 | 
			
		||||
 | 
			
		||||
      <Panel class="process-panel" position="top-left">
 | 
			
		||||
        <div class="layout-panel">
 | 
			
		||||
          <button v-if="isRunning" class="stop-btn" title="stop" @click="stop">
 | 
			
		||||
            <Icon name="stop" />
 | 
			
		||||
            <span class="spinner" />
 | 
			
		||||
          </button>
 | 
			
		||||
          <button v-else title="start" @click="run(nodes)">
 | 
			
		||||
            <Icon name="play" />
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <button title="set horizontal layout" @click="layoutGraph('LR')">
 | 
			
		||||
            <Icon name="horizontal" />
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <button title="set vertical layout" @click="layoutGraph('TB')">
 | 
			
		||||
            <Icon name="vertical" />
 | 
			
		||||
          </button>
 | 
			
		||||
 | 
			
		||||
          <button title="shuffle graph" @click="shuffleGraph">
 | 
			
		||||
            <Icon name="shuffle" />
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <!-- <div class="checkbox-panel">
 | 
			
		||||
          <label>Cancel on error</label>
 | 
			
		||||
          <input v-model="cancelOnError" type="checkbox" />
 | 
			
		||||
        </div> -->
 | 
			
		||||
      </Panel>
 | 
			
		||||
    </VueFlow>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
*,
 | 
			
		||||
::before,
 | 
			
		||||
::after {
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-content {
 | 
			
		||||
  margin: 0 !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.layout-flow {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-panel,
 | 
			
		||||
.layout-panel {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-panel {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  background-color: #2d3748;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 0 10px rgb(0 0 0 / 50%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-panel button {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 40px;
 | 
			
		||||
  height: 40px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  color: white;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  background-color: #4a5568;
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 0 10px rgb(0 0 0 / 50%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* .checkbox-panel {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
} */
 | 
			
		||||
 | 
			
		||||
.process-panel button:hover,
 | 
			
		||||
.layout-panel button:hover {
 | 
			
		||||
  background-color: #2563eb;
 | 
			
		||||
  transition: background-color 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-panel label {
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stop-btn svg {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stop-btn:hover svg {
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.stop-btn:hover .spinner {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spinner {
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  border: 3px solid #f3f3f3;
 | 
			
		||||
  border-top: 3px solid #2563eb;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										76
									
								
								src/views/vue-flow/layouting/initialElements.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/views/vue-flow/layouting/initialElements.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
			
		||||
import type { Edge, Node } from "@vue-flow/core";
 | 
			
		||||
 | 
			
		||||
const position = { x: 0, y: 0 };
 | 
			
		||||
const nodeType = "process";
 | 
			
		||||
const edgeType = "animation";
 | 
			
		||||
 | 
			
		||||
export const initialNodes: Node[] = [
 | 
			
		||||
  {
 | 
			
		||||
    id: "1",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "2",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "2a",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "2b",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "2c",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "2d",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "3",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "4",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "5",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "6",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "7",
 | 
			
		||||
    position,
 | 
			
		||||
    type: nodeType
 | 
			
		||||
  }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const initialEdges: Edge[] = [
 | 
			
		||||
  { id: "e1-2", source: "1", target: "2", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e1-3", source: "1", target: "3", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e2-2a", source: "2", target: "2a", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e2-2b", source: "2", target: "2b", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e2-2c", source: "2", target: "2c", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e2c-2d", source: "2c", target: "2d", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e3-7", source: "3", target: "4", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e4-5", source: "4", target: "5", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e5-6", source: "5", target: "6", type: edgeType, animated: true },
 | 
			
		||||
  { id: "e5-7", source: "5", target: "7", type: edgeType, animated: true }
 | 
			
		||||
];
 | 
			
		||||
							
								
								
									
										145
									
								
								src/views/vue-flow/layouting/processNode.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/views/vue-flow/layouting/processNode.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,145 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { toRef } from "vue";
 | 
			
		||||
import { Handle, useHandleConnections } from "@vue-flow/core";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  data: {
 | 
			
		||||
    type: Object,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  sourcePosition: {
 | 
			
		||||
    type: String
 | 
			
		||||
  },
 | 
			
		||||
  targetPosition: {
 | 
			
		||||
    type: String
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const sourceConnections = useHandleConnections({
 | 
			
		||||
  type: "target"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const targetConnections = useHandleConnections({
 | 
			
		||||
  type: "source"
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const isSender = toRef(() => sourceConnections.value.length <= 0);
 | 
			
		||||
 | 
			
		||||
const isReceiver = toRef(() => targetConnections.value.length <= 0);
 | 
			
		||||
 | 
			
		||||
const bgColor = toRef(() => {
 | 
			
		||||
  if (isSender.value) {
 | 
			
		||||
    return "#2563eb";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.hasError) {
 | 
			
		||||
    return "#f87171";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.isFinished) {
 | 
			
		||||
    return "#42B983";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.isCancelled) {
 | 
			
		||||
    return "#fbbf24";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return "#4b5563";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const processLabel = toRef(() => {
 | 
			
		||||
  if (props.data.hasError) {
 | 
			
		||||
    return "❌";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.isSkipped) {
 | 
			
		||||
    return "🚧";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.isCancelled) {
 | 
			
		||||
    return "🚫";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isSender.value) {
 | 
			
		||||
    return "📦";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (props.data.isFinished) {
 | 
			
		||||
    return "😎";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return "🏠";
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="process-node"
 | 
			
		||||
    :style="{
 | 
			
		||||
      backgroundColor: bgColor,
 | 
			
		||||
      boxShadow: data.isRunning ? '0 0 10px rgba(0, 0, 0, 0.5)' : ''
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <Handle v-if="!isSender" type="target" :position="targetPosition as any">
 | 
			
		||||
      <span
 | 
			
		||||
        v-if="
 | 
			
		||||
          !data.isRunning &&
 | 
			
		||||
          !data.isFinished &&
 | 
			
		||||
          !data.isCancelled &&
 | 
			
		||||
          !data.isSkipped &&
 | 
			
		||||
          !data.hasError
 | 
			
		||||
        "
 | 
			
		||||
        >📥
 | 
			
		||||
      </span>
 | 
			
		||||
    </Handle>
 | 
			
		||||
    <Handle
 | 
			
		||||
      v-if="!isReceiver"
 | 
			
		||||
      type="source"
 | 
			
		||||
      :position="sourcePosition as any"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <div v-if="!isSender && data.isRunning" class="spinner" />
 | 
			
		||||
    <span v-else>
 | 
			
		||||
      {{ processLabel }}
 | 
			
		||||
    </span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  0% {
 | 
			
		||||
    transform: rotate(0deg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: rotate(360deg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-node {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 24px;
 | 
			
		||||
  height: 24px;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  border-radius: 99px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.process-node .vue-flow__handle {
 | 
			
		||||
  width: unset;
 | 
			
		||||
  height: unset;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  background: transparent;
 | 
			
		||||
  border: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spinner {
 | 
			
		||||
  width: 20px;
 | 
			
		||||
  height: 20px;
 | 
			
		||||
  border: 1px solid #f3f3f3;
 | 
			
		||||
  border-top: 1px solid #2563eb;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										52
									
								
								src/views/vue-flow/layouting/useLayout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/views/vue-flow/layouting/useLayout.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
import dagre from "dagre";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { Position, useVueFlow } from "@vue-flow/core";
 | 
			
		||||
 | 
			
		||||
export function useLayout() {
 | 
			
		||||
  const { findNode } = useVueFlow();
 | 
			
		||||
 | 
			
		||||
  const graph = ref(new dagre.graphlib.Graph());
 | 
			
		||||
 | 
			
		||||
  const previousDirection = ref("LR");
 | 
			
		||||
 | 
			
		||||
  function layout(nodes, edges, direction) {
 | 
			
		||||
    const dagreGraph = new dagre.graphlib.Graph();
 | 
			
		||||
 | 
			
		||||
    graph.value = dagreGraph;
 | 
			
		||||
 | 
			
		||||
    dagreGraph.setDefaultEdgeLabel(() => ({}));
 | 
			
		||||
 | 
			
		||||
    const isHorizontal = direction === "LR";
 | 
			
		||||
    dagreGraph.setGraph({ rankdir: direction });
 | 
			
		||||
 | 
			
		||||
    previousDirection.value = direction;
 | 
			
		||||
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
      const graphNode = findNode(node.id);
 | 
			
		||||
 | 
			
		||||
      dagreGraph.setNode(node.id, {
 | 
			
		||||
        width: graphNode.dimensions.width || 150,
 | 
			
		||||
        height: graphNode.dimensions.height || 50
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const edge of edges) {
 | 
			
		||||
      dagreGraph.setEdge(edge.source, edge.target);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dagre.layout(dagreGraph);
 | 
			
		||||
 | 
			
		||||
    return nodes.map(node => {
 | 
			
		||||
      const nodeWithPosition = dagreGraph.node(node.id);
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        ...node,
 | 
			
		||||
        targetPosition: isHorizontal ? Position.Left : Position.Top,
 | 
			
		||||
        sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
 | 
			
		||||
        position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { graph, layout, previousDirection };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										181
									
								
								src/views/vue-flow/layouting/useRunProcess.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/views/vue-flow/layouting/useRunProcess.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,181 @@
 | 
			
		||||
import { ref, toRef, toValue } from "vue";
 | 
			
		||||
import { useVueFlow } from "@vue-flow/core";
 | 
			
		||||
 | 
			
		||||
export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
 | 
			
		||||
  const { updateNodeData, getConnectedEdges } = useVueFlow();
 | 
			
		||||
 | 
			
		||||
  const graph = toRef(() => toValue(dagreGraph));
 | 
			
		||||
 | 
			
		||||
  const isRunning = ref(false);
 | 
			
		||||
 | 
			
		||||
  const executedNodes = new Set();
 | 
			
		||||
 | 
			
		||||
  const runningTasks = new Map();
 | 
			
		||||
 | 
			
		||||
  const upcomingTasks = new Set();
 | 
			
		||||
 | 
			
		||||
  async function runNode(node, isStart = false) {
 | 
			
		||||
    if (executedNodes.has(node.id)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    upcomingTasks.add(node.id);
 | 
			
		||||
 | 
			
		||||
    const incomers = getConnectedEdges(node.id).filter(
 | 
			
		||||
      connection => connection.target === node.id
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      incomers.map(incomer => until(() => !incomer.data.isAnimating))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    upcomingTasks.clear();
 | 
			
		||||
 | 
			
		||||
    if (!isRunning.value) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    executedNodes.add(node.id);
 | 
			
		||||
 | 
			
		||||
    updateNodeData(node.id, {
 | 
			
		||||
      isRunning: true,
 | 
			
		||||
      isFinished: false,
 | 
			
		||||
      hasError: false,
 | 
			
		||||
      isCancelled: false
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const delay = Math.floor(Math.random() * 2000) + 1000;
 | 
			
		||||
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
      const timeout = setTimeout(
 | 
			
		||||
        async () => {
 | 
			
		||||
          const children = graph.value.successors(node.id);
 | 
			
		||||
 | 
			
		||||
          const willThrowError = Math.random() < 0.15;
 | 
			
		||||
 | 
			
		||||
          if (!isStart && willThrowError) {
 | 
			
		||||
            updateNodeData(node.id, { isRunning: false, hasError: true });
 | 
			
		||||
 | 
			
		||||
            if (toValue(cancelOnError)) {
 | 
			
		||||
              await skipDescendants(node.id);
 | 
			
		||||
              runningTasks.delete(node.id);
 | 
			
		||||
 | 
			
		||||
              // @ts-expect-error
 | 
			
		||||
              resolve();
 | 
			
		||||
              return;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          updateNodeData(node.id, { isRunning: false, isFinished: true });
 | 
			
		||||
 | 
			
		||||
          runningTasks.delete(node.id);
 | 
			
		||||
 | 
			
		||||
          if (children.length > 0) {
 | 
			
		||||
            await Promise.all(children.map(id => runNode({ id })));
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // @ts-expect-error
 | 
			
		||||
          resolve();
 | 
			
		||||
        },
 | 
			
		||||
        isStart ? 0 : delay
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      runningTasks.set(node.id, timeout);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function run(nodes) {
 | 
			
		||||
    if (isRunning.value) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    reset(nodes);
 | 
			
		||||
 | 
			
		||||
    isRunning.value = true;
 | 
			
		||||
 | 
			
		||||
    const startingNodes = nodes.filter(
 | 
			
		||||
      node => graph.value.predecessors(node.id)?.length === 0
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    await Promise.all(startingNodes.map(node => runNode(node, true)));
 | 
			
		||||
 | 
			
		||||
    clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function reset(nodes) {
 | 
			
		||||
    clear();
 | 
			
		||||
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
      updateNodeData(node.id, {
 | 
			
		||||
        isRunning: false,
 | 
			
		||||
        isFinished: false,
 | 
			
		||||
        hasError: false,
 | 
			
		||||
        isSkipped: false,
 | 
			
		||||
        isCancelled: false
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function skipDescendants(nodeId) {
 | 
			
		||||
    const children = graph.value.successors(nodeId);
 | 
			
		||||
 | 
			
		||||
    for (const child of children) {
 | 
			
		||||
      updateNodeData(child, { isRunning: false, isSkipped: true });
 | 
			
		||||
      await skipDescendants(child);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function stop() {
 | 
			
		||||
    isRunning.value = false;
 | 
			
		||||
 | 
			
		||||
    for (const nodeId of upcomingTasks) {
 | 
			
		||||
      clearTimeout(runningTasks.get(nodeId));
 | 
			
		||||
      runningTasks.delete(nodeId);
 | 
			
		||||
      // @ts-expect-error
 | 
			
		||||
      updateNodeData(nodeId, {
 | 
			
		||||
        isRunning: false,
 | 
			
		||||
        isFinished: false,
 | 
			
		||||
        hasError: false,
 | 
			
		||||
        isSkipped: false,
 | 
			
		||||
        isCancelled: true
 | 
			
		||||
      });
 | 
			
		||||
      await skipDescendants(nodeId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const [nodeId, task] of runningTasks) {
 | 
			
		||||
      clearTimeout(task);
 | 
			
		||||
      runningTasks.delete(nodeId);
 | 
			
		||||
      updateNodeData(nodeId, {
 | 
			
		||||
        isRunning: false,
 | 
			
		||||
        isFinished: false,
 | 
			
		||||
        hasError: false,
 | 
			
		||||
        isSkipped: false,
 | 
			
		||||
        isCancelled: true
 | 
			
		||||
      });
 | 
			
		||||
      await skipDescendants(nodeId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    executedNodes.clear();
 | 
			
		||||
    upcomingTasks.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function clear() {
 | 
			
		||||
    isRunning.value = false;
 | 
			
		||||
    executedNodes.clear();
 | 
			
		||||
    runningTasks.clear();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { run, stop, reset, isRunning };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function until(condition) {
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    const interval = setInterval(() => {
 | 
			
		||||
      if (condition()) {
 | 
			
		||||
        clearInterval(interval);
 | 
			
		||||
        // @ts-expect-error
 | 
			
		||||
        resolve();
 | 
			
		||||
      }
 | 
			
		||||
    }, 100);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								src/views/vue-flow/layouting/useShuffle.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/views/vue-flow/layouting/useShuffle.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
function shuffleArray(array) {
 | 
			
		||||
  for (let i = array.length - 1; i > 0; i--) {
 | 
			
		||||
    const j = Math.floor(Math.random() * (i + 1));
 | 
			
		||||
    [array[i], array[j]] = [array[j], array[i]];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generatePossibleEdges(nodes) {
 | 
			
		||||
  const possibleEdges = [];
 | 
			
		||||
 | 
			
		||||
  for (const sourceNode of nodes) {
 | 
			
		||||
    for (const targetNode of nodes) {
 | 
			
		||||
      if (sourceNode.id !== targetNode.id) {
 | 
			
		||||
        const edgeId = `e${sourceNode.id}-${targetNode.id}`;
 | 
			
		||||
        possibleEdges.push({
 | 
			
		||||
          id: edgeId,
 | 
			
		||||
          source: sourceNode.id,
 | 
			
		||||
          target: targetNode.id,
 | 
			
		||||
          type: "animation",
 | 
			
		||||
          animated: true
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return possibleEdges;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useShuffle() {
 | 
			
		||||
  return nodes => {
 | 
			
		||||
    const possibleEdges = generatePossibleEdges(nodes);
 | 
			
		||||
    shuffleArray(possibleEdges);
 | 
			
		||||
 | 
			
		||||
    const usedNodes = new Set();
 | 
			
		||||
    const newEdges = [];
 | 
			
		||||
 | 
			
		||||
    for (const edge of possibleEdges) {
 | 
			
		||||
      if (
 | 
			
		||||
        !usedNodes.has(edge.target) &&
 | 
			
		||||
        (usedNodes.size === 0 || usedNodes.has(edge.source))
 | 
			
		||||
      ) {
 | 
			
		||||
        newEdges.push(edge);
 | 
			
		||||
        usedNodes.add(edge.source);
 | 
			
		||||
        usedNodes.add(edge.target);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return newEdges;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user