diff --git a/package.json b/package.json index d97c6b499b..b87c3e5bd7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@pureadmin/descriptions": "^1.2.1", "@pureadmin/table": "^3.1.2", "@pureadmin/utils": "^2.4.7", + "@vue-flow/core": "^1.33.4", "@vueuse/core": "^10.9.0", "@vueuse/motion": "^2.1.0", "@wangeditor/editor": "^5.1.23", @@ -114,6 +115,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 +132,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc7ee20d1f..fcdd2e532b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: '@pureadmin/utils': specifier: ^2.4.7 version: 2.4.7(echarts@5.5.0)(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 +202,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 +253,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 +2012,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 +2365,20 @@ packages: path-browserify: 1.0.1 dev: true + /@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 +3878,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 +3951,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 +5061,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'} diff --git a/src/router/enums.ts b/src/router/enums.ts index 9670bffd8d..712277558a 100644 --- a/src/router/enums.ts +++ b/src/router/enums.ts @@ -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, diff --git a/src/router/modules/vueflow.ts b/src/router/modules/vueflow.ts new file mode 100644 index 0000000000..02f3892fd1 --- /dev/null +++ b/src/router/modules/vueflow.ts @@ -0,0 +1,21 @@ +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" + } + } + ] +} satisfies RouteConfigsTable; diff --git a/src/views/vue-flow/layouting/animationEdge.vue b/src/views/vue-flow/layouting/animationEdge.vue new file mode 100644 index 0000000000..64f8fbf4f3 --- /dev/null +++ b/src/views/vue-flow/layouting/animationEdge.vue @@ -0,0 +1,123 @@ + + + diff --git a/src/views/vue-flow/layouting/index.vue b/src/views/vue-flow/layouting/index.vue new file mode 100644 index 0000000000..1cd416cba8 --- /dev/null +++ b/src/views/vue-flow/layouting/index.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/views/vue-flow/layouting/initialElements.ts b/src/views/vue-flow/layouting/initialElements.ts new file mode 100644 index 0000000000..2a84ea4b74 --- /dev/null +++ b/src/views/vue-flow/layouting/initialElements.ts @@ -0,0 +1,81 @@ +import type { Edge, Node } from "@vue-flow/core"; + +const position = { x: 0, y: 0 }; +const type: string = "process"; + +export const initialNodes: Node[] = [ + { + id: "1", + position, + type + }, + { + id: "2", + position, + type + }, + { + id: "2a", + position, + type + }, + { + id: "2b", + position, + type + }, + { + id: "2c", + position, + type + }, + { + id: "2d", + position, + type + }, + { + id: "3", + position, + type + }, + { + id: "4", + position, + type + }, + { + id: "5", + position, + type + }, + { + id: "6", + position, + type + }, + { + id: "7", + position, + type + } +]; + +export const initialEdges: Edge[] = [ + { id: "e1-2", source: "1", target: "2", type: "animation", animated: true }, + { id: "e1-3", source: "1", target: "3", type: "animation", animated: true }, + { id: "e2-2a", source: "2", target: "2a", type: "animation", animated: true }, + { id: "e2-2b", source: "2", target: "2b", type: "animation", animated: true }, + { id: "e2-2c", source: "2", target: "2c", type: "animation", animated: true }, + { + id: "e2c-2d", + source: "2c", + target: "2d", + type: "animation", + animated: true + }, + { id: "e3-7", source: "3", target: "4", type: "animation", animated: true }, + { id: "e4-5", source: "4", target: "5", type: "animation", animated: true }, + { id: "e5-6", source: "5", target: "6", type: "animation", animated: true }, + { id: "e5-7", source: "5", target: "7", type: "animation", animated: true } +]; diff --git a/src/views/vue-flow/layouting/processNode.vue b/src/views/vue-flow/layouting/processNode.vue new file mode 100644 index 0000000000..0ff966ab57 --- /dev/null +++ b/src/views/vue-flow/layouting/processNode.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/views/vue-flow/layouting/useRunProcess.ts b/src/views/vue-flow/layouting/useRunProcess.ts new file mode 100644 index 0000000000..a87a923225 --- /dev/null +++ b/src/views/vue-flow/layouting/useRunProcess.ts @@ -0,0 +1,134 @@ +import type dagre from "dagre"; +import type { Node } from "@vue-flow/core"; +import { useVueFlow } from "@vue-flow/core"; +import { type MaybeRefOrGetter, toRef, toValue, ref } from "vue"; + +export function useRunProcess( + dagreGraph: MaybeRefOrGetter +) { + const { updateNodeData } = useVueFlow(); + + const graph = toRef(() => toValue(dagreGraph)); + + const isRunning = ref(false); + const executedNodes = new Set(); + const runningTasks = new Map(); + + async function runNode(node: { id: string }, isStart = false) { + if (executedNodes.has(node.id)) { + 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 + ) as unknown as string[]; + + const willThrowError = Math.random() < 0.15; + + if (willThrowError) { + updateNodeData(node.id, { isRunning: false, hasError: true }); + + await skipDescendants(node.id); + runningTasks.delete(node.id); + + 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 }))); + } + + resolve(); + }, + isStart ? 0 : delay + ); + + runningTasks.set(node.id, timeout); + }); + } + + async function run(nodes: Node[]) { + 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: Node[]) { + clear(); + + for (const node of nodes) { + updateNodeData(node.id, { + isRunning: false, + isFinished: false, + hasError: false, + isSkipped: false, + isCancelled: false + }); + } + } + + async function skipDescendants(nodeId: string) { + const children = graph.value.successors(nodeId) as unknown as string[]; + + for (const child of children) { + updateNodeData(child, { isRunning: false, isSkipped: true }); + await skipDescendants(child); + } + } + + function stop() { + isRunning.value = false; + + for (const [nodeId, task] of runningTasks) { + clearTimeout(task); + runningTasks.delete(nodeId); + updateNodeData(nodeId, { + isRunning: false, + isFinished: false, + hasError: false, + isSkipped: false, + isCancelled: true + }); + skipDescendants(nodeId); + } + + executedNodes.clear(); + } + + function clear() { + isRunning.value = false; + executedNodes.clear(); + runningTasks.clear(); + } + + return { run, stop, reset, isRunning }; +}