feat: migrate workflows to Vue Flow
This commit is contained in:
parent
1b0f414ba1
commit
320d1a9d92
@ -25,7 +25,7 @@
|
|||||||
| Element Plus | 2.13.1 |
|
| Element Plus | 2.13.1 |
|
||||||
| Pinia | 3.0.4 |
|
| Pinia | 3.0.4 |
|
||||||
| Vue Router | 4.6.4 |
|
| Vue Router | 4.6.4 |
|
||||||
| 可视化 | ECharts、LogicFlow、Three.js、Monaco Editor、XGPlayer |
|
| 可视化 | ECharts、Vue Flow、Three.js、Monaco Editor、XGPlayer |
|
||||||
|
|
||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
@ -100,4 +100,3 @@ src/views/corpusManger 语料管理
|
|||||||
- `src/settings.js`:布局、页签和版权配置。
|
- `src/settings.js`:布局、页签和版权配置。
|
||||||
|
|
||||||
生产密码、访问密钥和内部网络配置应通过部署环境注入,不要提交新的敏感信息。
|
生产密码、访问密钥和内部网络配置应通过部署环境注入,不要提交新的敏感信息。
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "2.3.2",
|
"@element-plus/icons-vue": "2.3.2",
|
||||||
"@logicflow/core": "^2.0.13",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@logicflow/extension": "^2.0.18",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
|
"@vue-flow/core": "^1.48.2",
|
||||||
"@vueup/vue-quill": "1.2.0",
|
"@vueup/vue-quill": "1.2.0",
|
||||||
"@vueuse/core": "14.1.0",
|
"@vueuse/core": "14.1.0",
|
||||||
"ace-builds": "^1.43.0",
|
"ace-builds": "^1.43.0",
|
||||||
|
|||||||
234
pnpm-lock.yaml
generated
234
pnpm-lock.yaml
generated
@ -14,12 +14,15 @@ importers:
|
|||||||
'@element-plus/icons-vue':
|
'@element-plus/icons-vue':
|
||||||
specifier: 2.3.2
|
specifier: 2.3.2
|
||||||
version: 2.3.2(vue@3.5.26)
|
version: 2.3.2(vue@3.5.26)
|
||||||
'@logicflow/core':
|
'@vue-flow/background':
|
||||||
specifier: ^2.0.13
|
specifier: ^1.3.2
|
||||||
version: 2.2.4
|
version: 1.3.2(@vue-flow/core@1.48.2(vue@3.5.26))(vue@3.5.26)
|
||||||
'@logicflow/extension':
|
'@vue-flow/controls':
|
||||||
specifier: ^2.0.18
|
specifier: ^1.1.3
|
||||||
version: 2.3.0(@logicflow/core@2.2.4)
|
version: 1.1.3(@vue-flow/core@1.48.2(vue@3.5.26))(vue@3.5.26)
|
||||||
|
'@vue-flow/core':
|
||||||
|
specifier: ^1.48.2
|
||||||
|
version: 1.48.2(vue@3.5.26)
|
||||||
'@vueup/vue-quill':
|
'@vueup/vue-quill':
|
||||||
specifier: 1.2.0
|
specifier: 1.2.0
|
||||||
version: 1.2.0(vue@3.5.26)
|
version: 1.2.0(vue@3.5.26)
|
||||||
@ -159,9 +162,6 @@ packages:
|
|||||||
'@antfu/utils@0.7.10':
|
'@antfu/utils@0.7.10':
|
||||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||||
|
|
||||||
'@antv/hierarchy@0.6.14':
|
|
||||||
resolution: {integrity: sha512-V3uknf7bhynOqQDw2sg+9r9DwZ9pc6k/EcqyTFdfXB1+ydr7urisP0MipIuimucvQKN+Qkd+d6w601r1UIroqQ==}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7':
|
'@babel/helper-string-parser@7.29.7':
|
||||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -440,14 +440,6 @@ packages:
|
|||||||
'@lezer/lr@1.4.10':
|
'@lezer/lr@1.4.10':
|
||||||
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
|
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
|
||||||
|
|
||||||
'@logicflow/core@2.2.4':
|
|
||||||
resolution: {integrity: sha512-lupgH9pUJ5cv3JB1+U+ahVwgQJyp1QBK6t3TXGOczzL6FwE53z8DYBj91WQQ2dFgDGShdcp4oSY1ZHMDhRZvmQ==}
|
|
||||||
|
|
||||||
'@logicflow/extension@2.3.0':
|
|
||||||
resolution: {integrity: sha512-7PGiWlGJ8oqXvHkfrr7vaMnLZgp4Ue6+TD371p8IxK2UbDQ6uWcJOoyij0pRpseU3+rL9oVyPo8qtIODKiaENQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@logicflow/core': 2.2.4
|
|
||||||
|
|
||||||
'@marijn/find-cluster-break@1.0.3':
|
'@marijn/find-cluster-break@1.0.3':
|
||||||
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
|
||||||
|
|
||||||
@ -751,6 +743,23 @@ packages:
|
|||||||
vite: ^5.0.0 || ^6.0.0
|
vite: ^5.0.0 || ^6.0.0
|
||||||
vue: ^3.2.25
|
vue: ^3.2.25
|
||||||
|
|
||||||
|
'@vue-flow/background@1.3.2':
|
||||||
|
resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue-flow/core': ^1.23.0
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
|
'@vue-flow/controls@1.1.3':
|
||||||
|
resolution: {integrity: sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue-flow/core': ^1.23.0
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
|
'@vue-flow/core@1.48.2':
|
||||||
|
resolution: {integrity: sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.3.0
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.26':
|
'@vue/compiler-core@3.5.26':
|
||||||
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
|
resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
|
||||||
|
|
||||||
@ -999,9 +1008,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
|
resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
classnames@2.5.1:
|
|
||||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
|
||||||
|
|
||||||
clipboard@2.0.11:
|
clipboard@2.0.11:
|
||||||
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
||||||
|
|
||||||
@ -1102,6 +1108,44 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
|
d3-color@3.1.0:
|
||||||
|
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1:
|
||||||
|
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-ease@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-selection@3.0.0:
|
||||||
|
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-timer@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
d3-transition@3.0.1:
|
||||||
|
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
d3-selection: 2 - 3
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
d@1.0.2:
|
d@1.0.2:
|
||||||
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
|
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@ -1582,9 +1626,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
hoist-non-react-statics@2.5.5:
|
|
||||||
resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==}
|
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
@ -1927,9 +1968,6 @@ packages:
|
|||||||
mdn-data@2.0.14:
|
mdn-data@2.0.14:
|
||||||
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
|
||||||
|
|
||||||
medium-editor@5.23.3:
|
|
||||||
resolution: {integrity: sha512-he9/TdjX8f8MGdXGfCs8AllrYnqXJJvjNkDKmPg3aPW/uoIrlRqtkFthrwvmd+u4QyzEiadhCCM0EwTiRdUCJw==}
|
|
||||||
|
|
||||||
memoize-one@6.0.0:
|
memoize-one@6.0.0:
|
||||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||||
|
|
||||||
@ -1978,26 +2016,9 @@ packages:
|
|||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
mobx-preact@3.0.0:
|
|
||||||
resolution: {integrity: sha512-ijan/cBs3WmRye87E5+3JmoFBB00KDAwNA3pm7bMwYLPHBAXlN86aC3gdrXw8aKzM5RI8V3a993PphzPv6P4FA==}
|
|
||||||
peerDependencies:
|
|
||||||
mobx: 5.x
|
|
||||||
preact: '>=8'
|
|
||||||
|
|
||||||
mobx-utils@5.6.2:
|
|
||||||
resolution: {integrity: sha512-a/WlXyGkp6F12b01sTarENpxbmlRgPHFyR1Xv2bsSjQBm5dcOtd16ONb40/vOqck8L99NHpI+C9MXQ+SZ8f+yw==}
|
|
||||||
peerDependencies:
|
|
||||||
mobx: ^4.13.1 || ^5.13.1
|
|
||||||
|
|
||||||
mobx@5.15.7:
|
|
||||||
resolution: {integrity: sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==}
|
|
||||||
|
|
||||||
monaco-editor@0.55.1:
|
monaco-editor@0.55.1:
|
||||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||||
|
|
||||||
mousetrap@1.6.5:
|
|
||||||
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
|
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
@ -2162,14 +2183,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-spBB5sgC4cv2YcW03f/IAUN1pgDJWNWD8FzkyY4mArLUMJW+KlQhlmUdKAHQuPfb00Jl5xIfImeOsf6YL8QK7Q==}
|
resolution: {integrity: sha512-spBB5sgC4cv2YcW03f/IAUN1pgDJWNWD8FzkyY4mArLUMJW+KlQhlmUdKAHQuPfb00Jl5xIfImeOsf6YL8QK7Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
preact@10.29.7:
|
|
||||||
resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==}
|
|
||||||
peerDependencies:
|
|
||||||
preact-render-to-string: '>=5'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
preact-render-to-string:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
proto-list@1.2.4:
|
proto-list@1.2.4:
|
||||||
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
|
||||||
|
|
||||||
@ -2200,9 +2213,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==}
|
resolution: {integrity: sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==}
|
||||||
engines: {npm: '>=8.2.3'}
|
engines: {npm: '>=8.2.3'}
|
||||||
|
|
||||||
rangy@1.3.2:
|
|
||||||
resolution: {integrity: sha512-fS1C4MOyk8T+ZJZdLcgrukPWxkyDXa+Hd2Kj+Zg4wIK71yrWgmjzHubzPMY1G+WD9EgGxMp3fIL0zQ1ickmSWA==}
|
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -2758,10 +2768,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
|
resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
uuid@9.0.1:
|
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
vanilla-jsoneditor@3.13.0:
|
vanilla-jsoneditor@3.13.0:
|
||||||
resolution: {integrity: sha512-u9C+D8Ocgr39Gnsfu7USjilfKssqbDqZehMdE0qbwIFnZFdNwkU4iKeEvJxvYbmlVi/sMLxMTrU14PhtXVyiZg==}
|
resolution: {integrity: sha512-u9C+D8Ocgr39Gnsfu7USjilfKssqbDqZehMdE0qbwIFnZFdNwkU4iKeEvJxvYbmlVi/sMLxMTrU14PhtXVyiZg==}
|
||||||
|
|
||||||
@ -2972,8 +2978,6 @@ snapshots:
|
|||||||
|
|
||||||
'@antfu/utils@0.7.10': {}
|
'@antfu/utils@0.7.10': {}
|
||||||
|
|
||||||
'@antv/hierarchy@0.6.14': {}
|
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7': {}
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7': {}
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
@ -3199,33 +3203,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@lezer/common': 1.5.2
|
'@lezer/common': 1.5.2
|
||||||
|
|
||||||
'@logicflow/core@2.2.4':
|
|
||||||
dependencies:
|
|
||||||
classnames: 2.5.1
|
|
||||||
lodash-es: 4.18.1
|
|
||||||
mobx: 5.15.7
|
|
||||||
mobx-preact: 3.0.0(mobx@5.15.7)(preact@10.29.7)
|
|
||||||
mobx-utils: 5.6.2(mobx@5.15.7)
|
|
||||||
mousetrap: 1.6.5
|
|
||||||
preact: 10.29.7
|
|
||||||
uuid: 9.0.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- preact-render-to-string
|
|
||||||
|
|
||||||
'@logicflow/extension@2.3.0(@logicflow/core@2.2.4)':
|
|
||||||
dependencies:
|
|
||||||
'@antv/hierarchy': 0.6.14
|
|
||||||
'@logicflow/core': 2.2.4
|
|
||||||
classnames: 2.5.1
|
|
||||||
lodash-es: 4.18.1
|
|
||||||
medium-editor: 5.23.3
|
|
||||||
mobx: 5.15.7
|
|
||||||
preact: 10.29.7
|
|
||||||
rangy: 1.3.2
|
|
||||||
vanilla-picker: 2.12.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- preact-render-to-string
|
|
||||||
|
|
||||||
'@marijn/find-cluster-break@1.0.3': {}
|
'@marijn/find-cluster-break@1.0.3': {}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
@ -3428,6 +3405,27 @@ snapshots:
|
|||||||
vite: 6.4.1(@types/node@26.1.2)(sass-embedded@1.97.2)(sass@1.97.2)(yaml@2.9.0)
|
vite: 6.4.1(@types/node@26.1.2)(sass-embedded@1.97.2)(sass@1.97.2)(yaml@2.9.0)
|
||||||
vue: 3.5.26
|
vue: 3.5.26
|
||||||
|
|
||||||
|
'@vue-flow/background@1.3.2(@vue-flow/core@1.48.2(vue@3.5.26))(vue@3.5.26)':
|
||||||
|
dependencies:
|
||||||
|
'@vue-flow/core': 1.48.2(vue@3.5.26)
|
||||||
|
vue: 3.5.26
|
||||||
|
|
||||||
|
'@vue-flow/controls@1.1.3(@vue-flow/core@1.48.2(vue@3.5.26))(vue@3.5.26)':
|
||||||
|
dependencies:
|
||||||
|
'@vue-flow/core': 1.48.2(vue@3.5.26)
|
||||||
|
vue: 3.5.26
|
||||||
|
|
||||||
|
'@vue-flow/core@1.48.2(vue@3.5.26)':
|
||||||
|
dependencies:
|
||||||
|
'@vueuse/core': 10.11.1(vue@3.5.26)
|
||||||
|
d3-drag: 3.0.0
|
||||||
|
d3-interpolate: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
d3-zoom: 3.0.0
|
||||||
|
vue: 3.5.26
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@vue/composition-api'
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.26':
|
'@vue/compiler-core@3.5.26':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
@ -3749,8 +3747,6 @@ snapshots:
|
|||||||
isobject: 3.0.1
|
isobject: 3.0.1
|
||||||
static-extend: 0.1.2
|
static-extend: 0.1.2
|
||||||
|
|
||||||
classnames@2.5.1: {}
|
|
||||||
|
|
||||||
clipboard@2.0.11:
|
clipboard@2.0.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
good-listener: 1.2.2
|
good-listener: 1.2.2
|
||||||
@ -3847,6 +3843,42 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
|
d3-color@3.1.0: {}
|
||||||
|
|
||||||
|
d3-dispatch@3.0.1: {}
|
||||||
|
|
||||||
|
d3-drag@3.0.0:
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch: 3.0.1
|
||||||
|
d3-selection: 3.0.0
|
||||||
|
|
||||||
|
d3-ease@3.0.1: {}
|
||||||
|
|
||||||
|
d3-interpolate@3.0.1:
|
||||||
|
dependencies:
|
||||||
|
d3-color: 3.1.0
|
||||||
|
|
||||||
|
d3-selection@3.0.0: {}
|
||||||
|
|
||||||
|
d3-timer@3.0.1: {}
|
||||||
|
|
||||||
|
d3-transition@3.0.1(d3-selection@3.0.0):
|
||||||
|
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
|
||||||
|
|
||||||
|
d3-zoom@3.0.0:
|
||||||
|
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)
|
||||||
|
|
||||||
d@1.0.2:
|
d@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es5-ext: 0.10.64
|
es5-ext: 0.10.64
|
||||||
@ -4443,8 +4475,6 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
hoist-non-react-statics@2.5.5: {}
|
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
htmlparser2@3.10.1:
|
htmlparser2@3.10.1:
|
||||||
@ -4763,8 +4793,6 @@ snapshots:
|
|||||||
|
|
||||||
mdn-data@2.0.14: {}
|
mdn-data@2.0.14: {}
|
||||||
|
|
||||||
medium-editor@5.23.3: {}
|
|
||||||
|
|
||||||
memoize-one@6.0.0: {}
|
memoize-one@6.0.0: {}
|
||||||
|
|
||||||
merge-options@1.0.1:
|
merge-options@1.0.1:
|
||||||
@ -4824,25 +4852,11 @@ snapshots:
|
|||||||
pkg-types: 1.3.1
|
pkg-types: 1.3.1
|
||||||
ufo: 1.6.4
|
ufo: 1.6.4
|
||||||
|
|
||||||
mobx-preact@3.0.0(mobx@5.15.7)(preact@10.29.7):
|
|
||||||
dependencies:
|
|
||||||
hoist-non-react-statics: 2.5.5
|
|
||||||
mobx: 5.15.7
|
|
||||||
preact: 10.29.7
|
|
||||||
|
|
||||||
mobx-utils@5.6.2(mobx@5.15.7):
|
|
||||||
dependencies:
|
|
||||||
mobx: 5.15.7
|
|
||||||
|
|
||||||
mobx@5.15.7: {}
|
|
||||||
|
|
||||||
monaco-editor@0.55.1:
|
monaco-editor@0.55.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
dompurify: 3.2.7
|
dompurify: 3.2.7
|
||||||
marked: 14.0.0
|
marked: 14.0.0
|
||||||
|
|
||||||
mousetrap@1.6.5: {}
|
|
||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
@ -5008,8 +5022,6 @@ snapshots:
|
|||||||
posthtml-parser: 0.2.1
|
posthtml-parser: 0.2.1
|
||||||
posthtml-render: 1.4.0
|
posthtml-render: 1.4.0
|
||||||
|
|
||||||
preact@10.29.7: {}
|
|
||||||
|
|
||||||
proto-list@1.2.4: {}
|
proto-list@1.2.4: {}
|
||||||
|
|
||||||
proxy-from-env@1.1.0: {}
|
proxy-from-env@1.1.0: {}
|
||||||
@ -5044,8 +5056,6 @@ snapshots:
|
|||||||
parchment: 3.0.0
|
parchment: 3.0.0
|
||||||
quill-delta: 5.1.0
|
quill-delta: 5.1.0
|
||||||
|
|
||||||
rangy@1.3.2: {}
|
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
@ -5732,8 +5742,6 @@ snapshots:
|
|||||||
|
|
||||||
uuid@11.1.1: {}
|
uuid@11.1.1: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
|
||||||
|
|
||||||
vanilla-jsoneditor@3.13.0:
|
vanilla-jsoneditor@3.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@codemirror/autocomplete': 6.20.3
|
'@codemirror/autocomplete': 6.20.3
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export function getInst(query) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询任务实例日志
|
// 查询任务实例日志
|
||||||
export function getLogicFlow(query) {
|
export function getFlowRuntimeLog(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/test/inst/log/query',
|
url: '/test/inst/log/query',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|||||||
@ -44,6 +44,8 @@ import ImageUpload from "@/components/ImageUpload"
|
|||||||
import ImagePreview from "@/components/ImagePreview"
|
import ImagePreview from "@/components/ImagePreview"
|
||||||
// 字典标签组件
|
// 字典标签组件
|
||||||
import DictTag from '@/components/DictTag'
|
import DictTag from '@/components/DictTag'
|
||||||
|
import { JsonViewer } from 'vue3-json-viewer'
|
||||||
|
import 'vue3-json-viewer/dist/index.css'
|
||||||
|
|
||||||
import ace from 'ace-builds'
|
import ace from 'ace-builds'
|
||||||
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
|
||||||
@ -71,6 +73,7 @@ app.component('ImageUpload', ImageUpload)
|
|||||||
app.component('ImagePreview', ImagePreview)
|
app.component('ImagePreview', ImagePreview)
|
||||||
app.component('RightToolbar', RightToolbar)
|
app.component('RightToolbar', RightToolbar)
|
||||||
app.component('Editor', Editor)
|
app.component('Editor', Editor)
|
||||||
|
app.component('JsonViewer', JsonViewer)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
|
|||||||
@ -135,18 +135,18 @@ export const constantRoutes = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
//logicFlow
|
// 流程运行状态
|
||||||
path: "/logic_flow",
|
path: "/logic_flow",
|
||||||
component: () =>
|
component: () =>
|
||||||
import("@/views/device/register/components/LogicFlow/index.vue"),
|
import("@/views/device/register/components/VueFlow/index.vue"),
|
||||||
meta: { title: "logicFlow", noLogin: true },
|
meta: { title: "流程运行状态", noLogin: true },
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
//logicFlow
|
// 流程编排
|
||||||
path: "/flow",
|
path: "/flow",
|
||||||
component: () => import("@/views/flow/index.vue"),
|
component: () => import("@/views/flow/index.vue"),
|
||||||
meta: { title: "flow", noLogin: true },
|
meta: { title: "流程编排", noLogin: true },
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,105 +0,0 @@
|
|||||||
|
|
||||||
import { getFlowById } from '@/api/device/register'
|
|
||||||
|
|
||||||
const filterNodeRedData = async (id) => {
|
|
||||||
let nodeRedData = []
|
|
||||||
const data = await getFlowById(id)
|
|
||||||
if (data.nodes) {
|
|
||||||
nodeRedData = data.nodes
|
|
||||||
} else {
|
|
||||||
console.error('请求失败')
|
|
||||||
}
|
|
||||||
return nodeRedData
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapNodeType = (nodeRedType) => {
|
|
||||||
const typeMap = {
|
|
||||||
'group': 'customGroup', // Node-RED的inject节点转为圆形
|
|
||||||
// 'inject': 'circle', // Node-RED的inject节点转为圆形
|
|
||||||
// 'debug': 'diamond', // debug节点转为菱形
|
|
||||||
// 'function': 'rect' // 函数节点转为矩形
|
|
||||||
};
|
|
||||||
return typeMap[nodeRedType] || 'CustomNode'; // 默认矩形
|
|
||||||
}
|
|
||||||
|
|
||||||
export const convertNodeRedToLogicFlow = async (id) => {
|
|
||||||
const nodeRedData = await filterNodeRedData(id)
|
|
||||||
// 节点数据
|
|
||||||
const nodes = nodeRedData.map((node) => {
|
|
||||||
return {
|
|
||||||
id: node.id, // 或生成新ID:`node-${index}`
|
|
||||||
type: mapNodeType(node.type), // 映射节点类型
|
|
||||||
x: node.x * 2,
|
|
||||||
y: node.y * 2,
|
|
||||||
text: node.name || node.type, // 使用节点名称或类型作为文本
|
|
||||||
children: node.type === 'group' ? node.nodes : null,
|
|
||||||
properties: {
|
|
||||||
...node,
|
|
||||||
state: 'unexecuted',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const edges = []
|
|
||||||
// 连线数据
|
|
||||||
nodeRedData.forEach((item) => {
|
|
||||||
if (item.wires && item.wires.length > 0) {
|
|
||||||
item.wires.forEach((w, index) => {
|
|
||||||
w.forEach((id) => {
|
|
||||||
const obj = {
|
|
||||||
id: `edge-${item.id}-${id}`, // 或使用唯一标识生成方式
|
|
||||||
type: 'bezier',
|
|
||||||
sourceNodeId: item.id,
|
|
||||||
targetNodeId: id,
|
|
||||||
sourceAnchorId: item.id + '_1',
|
|
||||||
targetAnchorId: id + '_3'
|
|
||||||
}
|
|
||||||
edges.push(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return { nodes, edges }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const recursiveFilter = (data, id, result = [], type = 'customGroup') => {
|
|
||||||
data.forEach((item) => {
|
|
||||||
if (item.type === type && item.properties.nodes && item.properties.nodes.includes(id)) {
|
|
||||||
result.push(item)
|
|
||||||
recursiveFilter(data, item.id, result)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export const changeOtherNodeState = (nodes, lf) => {
|
|
||||||
const filterArr = ['inject', 'cmvrLoop', 'startCamera', 'getImage', 'stopCamera', 'startVideo', 'stopVideo', 'end']
|
|
||||||
const loopNodes = nodes.filter(item => {
|
|
||||||
return !filterArr.includes(item.properties.type)
|
|
||||||
})
|
|
||||||
loopNodes.forEach(l => {
|
|
||||||
nodes.forEach(n => {
|
|
||||||
if (l.properties.wires[0].includes(n.id)) {
|
|
||||||
const { state } = lf.getProperties(n.id)
|
|
||||||
if (state !== 'unexecuted') {
|
|
||||||
lf.setProperties(l.id, {
|
|
||||||
state: 'success'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (l.properties?.wires[1]?.includes(n.id)) {
|
|
||||||
const { state } = lf.getProperties(n.id)
|
|
||||||
if (state !== 'failed') {
|
|
||||||
lf.setProperties(l.id, {
|
|
||||||
state: 'running'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
lf.setProperties(l.id, {
|
|
||||||
state: 'failed'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
76
src/utils/convertNodeRedToVueFlow.js
Normal file
76
src/utils/convertNodeRedToVueFlow.js
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { getFlowById } from "@/api/device/register";
|
||||||
|
|
||||||
|
const loadNodeRedData = async (id) => {
|
||||||
|
const data = await getFlowById(id);
|
||||||
|
return Array.isArray(data?.nodes) ? data.nodes : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertNodeRedToVueFlow = async (id) => {
|
||||||
|
const source = await loadNodeRedData(id);
|
||||||
|
const nodes = source.map((node) => {
|
||||||
|
const isGroup = node.type === "group";
|
||||||
|
const width = isGroup ? Number(node.w || 300) + 80 : 200;
|
||||||
|
const height = isGroup ? Number(node.h || 180) + 80 : 100;
|
||||||
|
const center = { x: Number(node.x || 0) * 2, y: Number(node.y || 0) * 2 };
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
type: isGroup ? "runtime-group" : "runtime-node",
|
||||||
|
position: { x: center.x - width / 2, y: center.y - height / 2 },
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
draggable: false,
|
||||||
|
selectable: true,
|
||||||
|
connectable: false,
|
||||||
|
deletable: false,
|
||||||
|
zIndex: isGroup ? -1 : 1,
|
||||||
|
data: {
|
||||||
|
text: node.name || node.type,
|
||||||
|
properties: { ...node, state: "unexecuted" },
|
||||||
|
children: isGroup ? node.nodes || [] : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const edges = [];
|
||||||
|
source.forEach((node) => {
|
||||||
|
(node.wires || []).forEach((targets) => {
|
||||||
|
(targets || []).forEach((targetId) => {
|
||||||
|
edges.push({
|
||||||
|
id: `edge-${node.id}-${targetId}`,
|
||||||
|
source: node.id,
|
||||||
|
target: targetId,
|
||||||
|
sourceHandle: `${node.id}_1`,
|
||||||
|
targetHandle: `${targetId}_3`,
|
||||||
|
type: "bezier",
|
||||||
|
animated: true,
|
||||||
|
selectable: true,
|
||||||
|
focusable: true,
|
||||||
|
interactionWidth: 32,
|
||||||
|
markerEnd: {
|
||||||
|
type: "arrowclosed",
|
||||||
|
color: "#2f80ed",
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
style: { stroke: "#2f80ed", strokeWidth: 2 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDerivedNodeStates = (nodes, getProperties, setProperties) => {
|
||||||
|
const ignored = ["inject", "cmvrLoop", "startCamera", "getImage", "stopCamera", "startVideo", "stopVideo", "end"];
|
||||||
|
nodes.filter((node) => !ignored.includes(node.data.properties.type)).forEach((current) => {
|
||||||
|
nodes.forEach((target) => {
|
||||||
|
if (current.data.properties.wires?.[0]?.includes(target.id) && getProperties(target.id).state !== "unexecuted") {
|
||||||
|
setProperties(current.id, { state: "success" });
|
||||||
|
}
|
||||||
|
if (current.data.properties.wires?.[1]?.includes(target.id)) {
|
||||||
|
setProperties(current.id, { state: getProperties(target.id).state === "failed" ? "failed" : "running" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,103 +0,0 @@
|
|||||||
|
|
||||||
import { GroupNodeModel, GroupNode } from '@logicflow/extension'
|
|
||||||
import { recursiveFilter } from "@/utils/convertNodeRedToLogicFlow";
|
|
||||||
|
|
||||||
class CustomGroupModel extends GroupNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.children = new Set(data.children || [])
|
|
||||||
this.zIndex = 0;
|
|
||||||
|
|
||||||
this.width = data.w + 80; // 增加内边距
|
|
||||||
this.height = data.h + 80;
|
|
||||||
this.x = data.x;
|
|
||||||
this.y = data.y;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateSize()
|
|
||||||
const nodes = this.graphModel.nodes;
|
|
||||||
const arr = recursiveFilter(nodes, this.id)
|
|
||||||
arr.forEach((item) => {
|
|
||||||
item.updateSize()
|
|
||||||
})
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttributes() {
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextStyle() {
|
|
||||||
const style = super.getTextStyle();
|
|
||||||
style.fill = this.properties.style?.color || '#333';
|
|
||||||
style.fontSize = 16;
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize() {
|
|
||||||
const children = []
|
|
||||||
this.children.forEach((id) => {
|
|
||||||
children.push(this.graphModel.getNodeModelById(id))
|
|
||||||
});
|
|
||||||
if (children.length === 0) return;
|
|
||||||
|
|
||||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
||||||
children.forEach((node) => {
|
|
||||||
minX = Math.min(minX, node.x - node.width / 2);
|
|
||||||
maxX = Math.max(maxX, node.x + node.width / 2);
|
|
||||||
minY = Math.min(minY, node.y - node.height / 2);
|
|
||||||
maxY = Math.max(maxY, node.y + node.height / 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.width = maxX - minX + 80; // 增加内边距
|
|
||||||
this.height = maxY - minY + 80;
|
|
||||||
this.x = (minX + maxX) / 2;
|
|
||||||
this.y = (minY + maxY) / 2;
|
|
||||||
this.setTextCoordinate(this.width, this.height, this.x, this.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTextCoordinate(width, height, x, y) {
|
|
||||||
const strategies = {
|
|
||||||
"n": () => {
|
|
||||||
this.text.x = x
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
},
|
|
||||||
"ne": () => {
|
|
||||||
this.text.x = x + width / 2 - 20
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
},
|
|
||||||
"sw": () => {
|
|
||||||
this.text.x = x - width / 2 + 20
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"s": () => {
|
|
||||||
this.text.x = x
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"se": () => {
|
|
||||||
this.text.x = x + width / 2 - 20
|
|
||||||
this.text.y = y + height / 2 - 20
|
|
||||||
},
|
|
||||||
"default": () => {
|
|
||||||
this.text.x = x - width / 2 + 20
|
|
||||||
this.text.y = y - height / 2 + 20
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const labelPosition = this.properties.style?.['label-position'] || 'default'
|
|
||||||
const action = strategies[labelPosition]
|
|
||||||
action();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChild(id) {
|
|
||||||
this.children.delete(id) // 现在可安全调用
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCustomGroup(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "customGroup",
|
|
||||||
view: GroupNode,
|
|
||||||
model: CustomGroupModel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from '@logicflow/core';
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h } from 'vue';
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import CustomFlow from './customFlow.vue';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class CustomFlowHtmlNode extends HtmlNode {
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.isMounted = false;
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(CustomFlow, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
text: props.model.text.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.style.width = '100%'
|
|
||||||
node.style.height = '100%'
|
|
||||||
rootEl.appendChild(node);
|
|
||||||
this.app.use(ElementPlus) // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(node);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class CustomFlowHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
// 仅允许从底部锚点连出
|
|
||||||
this.sourceRules.push({
|
|
||||||
message: '只能从输出锚点连线',
|
|
||||||
validate: (sourceNode, targetNode, sourceAnchor) =>
|
|
||||||
// sourceAnchor.name === 'right'
|
|
||||||
sourceAnchor.properties.connectionType === 'source'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 仅允许连接到顶部锚点
|
|
||||||
this.targetRules.push({
|
|
||||||
message: '只能连接到输入锚点',
|
|
||||||
validate: (sourceNode, targetNode, targetAnchor) =>
|
|
||||||
// targetAnchor.name === 'left'
|
|
||||||
targetAnchor.properties.connectionType === 'target'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
this.width = 200;
|
|
||||||
this.height = 100;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this, x = _a.x, y = _a.y, width = _a.width, height = _a.height;
|
|
||||||
return [
|
|
||||||
// { x: x, y: y - height / 2, id: "".concat(this.id, "_0") },
|
|
||||||
{ x: x + width / 2, y: y, name: 'left', id: "".concat(this.id, "_1"), properties: { connectionType: 'target' } },
|
|
||||||
// { x: x, y: y + height / 2, id: "".concat(this.id, "_2") },
|
|
||||||
{ x: x - width / 2, y: y, name: 'right', id: "".concat(this.id, "_3"), properties: { connectionType: 'source' } },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = 'none';
|
|
||||||
// style.hover.stroke = 'none';
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCustomNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "CustomNode",
|
|
||||||
view: CustomFlowHtmlNode,
|
|
||||||
model: CustomFlowHtmlModel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flow_container">
|
|
||||||
<div class="btn_container">
|
|
||||||
<div class="instState">实例状态:<span :class="instStatus">{{ stateMap[instStatus]}}</span></div>
|
|
||||||
<el-button key=" translateCenter" type="primary" @click="() => lfRef.translateCenter()">居中</el-button>
|
|
||||||
<el-button key="fitView" type="primary" @click="() => lfRef.fitView()">适应屏幕</el-button>
|
|
||||||
<el-button v-if="['RUNNING','PAUSED'].includes(instStatus)" type="primary" @click="stopFlow">停止</el-button>
|
|
||||||
<el-button v-if="instStatus === 'RUNNING'" type="primary" @click="pauseFlow">暂停</el-button>
|
|
||||||
<el-button v-if="instStatus === 'PAUSED'" type="primary" @click="restartFlow">恢复</el-button>
|
|
||||||
</div>
|
|
||||||
<div ref="containerRef" class="containerRef"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import LogicFlow from "@logicflow/core";
|
|
||||||
import { Group } from "@logicflow/extension";
|
|
||||||
import "@logicflow/core/es/index.css";
|
|
||||||
import { convertNodeRedToLogicFlow, recursiveFilter, changeOtherNodeState } from '@/utils/convertNodeRedToLogicFlow'
|
|
||||||
import { registerCustomNode } from "./customFlowNode";
|
|
||||||
import { registerCustomGroup } from "./CustomGroup";
|
|
||||||
import { getLogicFlow } from '@/api/test/log'
|
|
||||||
import { pauseInst, resumeInst, stopInst } from '@/api/device/terminal'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const containerRef = ref(null);
|
|
||||||
const lfRef = ref(null);
|
|
||||||
// 在新窗口的脚本中
|
|
||||||
const queryParams = new URLSearchParams(window.location.search);
|
|
||||||
const instId = queryParams.get('instId') || '';
|
|
||||||
const taskId = queryParams.get('taskId') || '';
|
|
||||||
const itemId = queryParams.get('itemId') || '03e0842c2fd7ab7575636a473aeefc89';
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: "custom-node-1",
|
|
||||||
text: "node-1",
|
|
||||||
type: "CustomNode",
|
|
||||||
x: 100,
|
|
||||||
y: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'custom-node-2',
|
|
||||||
type: 'CustomNode',
|
|
||||||
x: 300,
|
|
||||||
y: 300,
|
|
||||||
text: 'node-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// 边
|
|
||||||
edges: [
|
|
||||||
{
|
|
||||||
type: 'polyline',
|
|
||||||
sourceNodeId: 'custom-node-1',
|
|
||||||
targetNodeId: 'custom-node-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataNodes = ref([])
|
|
||||||
|
|
||||||
const loadNodeRedData = async () => {
|
|
||||||
const {nodes, edges} = await convertNodeRedToLogicFlow(itemId)
|
|
||||||
lfRef.value.render({
|
|
||||||
nodes,
|
|
||||||
edges
|
|
||||||
})
|
|
||||||
dataNodes.value = nodes
|
|
||||||
setTimeout(() => {
|
|
||||||
lfRef.value.fitView()
|
|
||||||
}, 20)
|
|
||||||
getLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLog = async () => {
|
|
||||||
const res = await getLogicFlow({
|
|
||||||
instId,
|
|
||||||
taskId,
|
|
||||||
itemId
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
|
||||||
const arr = res.data.filter(item => {
|
|
||||||
return item.nodeType
|
|
||||||
})
|
|
||||||
const map = new Map()
|
|
||||||
arr.forEach(item => {
|
|
||||||
if (['inject', 'start'].includes(item.nodeType)) { // , 'end'
|
|
||||||
map.set(item.nodeId, {
|
|
||||||
state: 'success',
|
|
||||||
nodeType: item.nodeType,
|
|
||||||
input: item.params
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let obj
|
|
||||||
if (map.has(item.nodeId)) {
|
|
||||||
obj = map.get(item.nodeId)
|
|
||||||
} else {
|
|
||||||
obj = {
|
|
||||||
nodeType: item.nodeType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.paramsType === 'INPUT') {
|
|
||||||
obj.state = item.instStatus.toLowerCase(),
|
|
||||||
obj.input = item.instStatus === 'FAILED' ? item.params : item.message
|
|
||||||
} else {
|
|
||||||
obj.state = item.instStatus.toLowerCase() === 'running' ? 'success' : item.instStatus.toLowerCase(),
|
|
||||||
obj.output = item.instStatus === 'FAILED' ? item.message : item.params
|
|
||||||
}
|
|
||||||
map.set(item.nodeId, obj)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const [key, value] of map) {
|
|
||||||
lfRef.value.setProperties(key, value)
|
|
||||||
}
|
|
||||||
changeOtherNodeState(dataNodes.value, lfRef.value)
|
|
||||||
if (arr[arr.length - 1].nodeType !== 'end' && arr[arr.length - 1].instStatus !== 'FAILED') {
|
|
||||||
setTimeout(() => {
|
|
||||||
getLog()
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopFlow = async () => {
|
|
||||||
const res = await stopInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('停止成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pauseFlow = async () => {
|
|
||||||
const res = await pauseInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('暂停成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const restartFlow = async () => {
|
|
||||||
const res = await resumeInst(instId)
|
|
||||||
if (res.code === 200) {
|
|
||||||
ElMessage.success('恢复成功')
|
|
||||||
getInstDetail()
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const instStatus = ref('RUNNING')
|
|
||||||
const stateMap = {
|
|
||||||
SUCCESS: "已完成",
|
|
||||||
RUNNING: "执行中",
|
|
||||||
FAILED: "失败",
|
|
||||||
PAUSED: "已暂停",
|
|
||||||
STOPPED: "已终止"
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInstDetail = async () => {
|
|
||||||
const res = await getLogicFlow({
|
|
||||||
instId,
|
|
||||||
taskId,
|
|
||||||
itemId
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
|
||||||
const item = res.data[res.data.length - 1]
|
|
||||||
instStatus.value = item.instStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (containerRef.value) {
|
|
||||||
const lf = new LogicFlow({
|
|
||||||
container: containerRef.value,
|
|
||||||
isSilentMode: true,
|
|
||||||
adjustNodePosition: true,
|
|
||||||
stopZoomGraph: false,
|
|
||||||
stopScrollGraph: false,
|
|
||||||
stopMoveGraph: false,
|
|
||||||
autoExpand: true,
|
|
||||||
adjustEdgeStartAndEnd: true,
|
|
||||||
allowRotate: false,
|
|
||||||
edgeTextEdit: false,
|
|
||||||
keyboard: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
partial: true,
|
|
||||||
background: {
|
|
||||||
color: "#FFFFFF",
|
|
||||||
},
|
|
||||||
grid: true,
|
|
||||||
edgeTextDraggable: false,
|
|
||||||
nodeTextEdit: false, //节点是否可编辑。false不可编辑
|
|
||||||
textEdit: false, //是否开启文本编辑
|
|
||||||
style: {
|
|
||||||
inputText: {
|
|
||||||
background: "black",
|
|
||||||
color: "white",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
idGenerator(type) {
|
|
||||||
return type + "_" + Math.random();
|
|
||||||
},
|
|
||||||
plugins: [Group],
|
|
||||||
group: {
|
|
||||||
foldable: true, // 启用折叠功能
|
|
||||||
foldSize: 30, // 折叠后显示的图标尺寸
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 注册自定义节点
|
|
||||||
registerCustomNode(lf)
|
|
||||||
registerCustomGroup(lf)
|
|
||||||
// 渲染数据 data
|
|
||||||
lf.render(data);
|
|
||||||
|
|
||||||
lf.on('node:dragstart', (data, e) => {
|
|
||||||
lfRef.value.setProperties(data.data.id, {
|
|
||||||
nowTime: new Date().getTime()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听子节点拖动事件
|
|
||||||
lf.on("node:mousemove", ({ data, e }) => {
|
|
||||||
const { nodes } = lf.getGraphData();
|
|
||||||
const arr = recursiveFilter(nodes, data.id)
|
|
||||||
arr.forEach(item => {
|
|
||||||
lf.getNodeModelById(item.id).updateSize()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听子节点拖动事件
|
|
||||||
lf.on("blank:drop", () => {
|
|
||||||
getLog()
|
|
||||||
});
|
|
||||||
|
|
||||||
lfRef.value = lf;
|
|
||||||
}
|
|
||||||
loadNodeRedData()
|
|
||||||
getInstDetail()
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.flow_container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.btn_container {
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: end;
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
.instState {
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
.RUNNING, .PAUSED, .STOPPED {
|
|
||||||
color: #8bbf86;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SUCCESS {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.FAILED {
|
|
||||||
color: rgb(128, 41, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.containerRef) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.lf-graph {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div class="runtime-group">
|
||||||
|
<span>{{ data.text || data.properties?.name || "分组" }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({ data: Object });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.runtime-group {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 14px 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px dashed #91a4be;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(234, 241, 250, 0.45);
|
||||||
|
color: #40536d;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
47
src/views/device/register/components/VueFlow/RuntimeNode.vue
Normal file
47
src/views/device/register/components/VueFlow/RuntimeNode.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="runtime-node">
|
||||||
|
<Handle
|
||||||
|
:id="`${id}_3`"
|
||||||
|
type="target"
|
||||||
|
:position="Position.Left"
|
||||||
|
:connectable="false"
|
||||||
|
class="runtime-handle runtime-handle--target"
|
||||||
|
/>
|
||||||
|
<CustomFlow :properties="properties" :text="data.text" />
|
||||||
|
<Handle
|
||||||
|
:id="`${id}_1`"
|
||||||
|
type="source"
|
||||||
|
:position="Position.Right"
|
||||||
|
:connectable="false"
|
||||||
|
class="runtime-handle runtime-handle--source"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { Handle, Position } from "@vue-flow/core";
|
||||||
|
import CustomFlow from "./customFlow.vue";
|
||||||
|
|
||||||
|
const props = defineProps({ id: String, data: Object });
|
||||||
|
const properties = computed(() => props.data.properties || {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.runtime-node {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-handle {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
background: #2f80ed;
|
||||||
|
&--target { left: -6px; }
|
||||||
|
&--source { right: -6px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
206
src/views/device/register/components/VueFlow/index.vue
Normal file
206
src/views/device/register/components/VueFlow/index.vue
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<div class="runtime-flow-page">
|
||||||
|
<div class="runtime-toolbar">
|
||||||
|
<div class="instance-state">
|
||||||
|
实例状态:<span :class="instStatus">{{ stateMap[instStatus] || instStatus }}</span>
|
||||||
|
</div>
|
||||||
|
<el-button @click="centerView">居中</el-button>
|
||||||
|
<el-button @click="fitGraph">适应屏幕</el-button>
|
||||||
|
<el-button v-if="['RUNNING', 'PAUSED'].includes(instStatus)" type="danger" plain @click="stopFlow">停止</el-button>
|
||||||
|
<el-button v-if="instStatus === 'RUNNING'" type="warning" plain @click="pauseFlow">暂停</el-button>
|
||||||
|
<el-button v-if="instStatus === 'PAUSED'" type="success" plain @click="restartFlow">恢复</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VueFlow
|
||||||
|
id="runtime-flow"
|
||||||
|
class="runtime-canvas"
|
||||||
|
:nodes-draggable="false"
|
||||||
|
:nodes-connectable="false"
|
||||||
|
:edges-updatable="false"
|
||||||
|
:elements-selectable="true"
|
||||||
|
:delete-key-code="null"
|
||||||
|
:min-zoom="0.1"
|
||||||
|
:max-zoom="3"
|
||||||
|
:zoom-on-double-click="false"
|
||||||
|
@init="loadFlow"
|
||||||
|
@pane-click="closeNodeDetails"
|
||||||
|
>
|
||||||
|
<template #node-runtime-node="nodeProps">
|
||||||
|
<RuntimeNode v-bind="nodeProps" />
|
||||||
|
</template>
|
||||||
|
<template #node-runtime-group="nodeProps">
|
||||||
|
<RuntimeGroup v-bind="nodeProps" />
|
||||||
|
</template>
|
||||||
|
<Background color="#d9dee8" :gap="24" :size="1" />
|
||||||
|
<Controls position="bottom-right" :show-interactive="false" />
|
||||||
|
</VueFlow>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onUnmounted, ref } from "vue";
|
||||||
|
import { VueFlow, useVueFlow } from "@vue-flow/core";
|
||||||
|
import { Background } from "@vue-flow/background";
|
||||||
|
import { Controls } from "@vue-flow/controls";
|
||||||
|
import "@vue-flow/core/dist/style.css";
|
||||||
|
import "@vue-flow/core/dist/theme-default.css";
|
||||||
|
import "@vue-flow/controls/dist/style.css";
|
||||||
|
import RuntimeNode from "./RuntimeNode.vue";
|
||||||
|
import RuntimeGroup from "./RuntimeGroup.vue";
|
||||||
|
import { convertNodeRedToVueFlow, updateDerivedNodeStates } from "@/utils/convertNodeRedToVueFlow";
|
||||||
|
import { getFlowRuntimeLog } from "@/api/test/log";
|
||||||
|
import { pauseInst, resumeInst, stopInst } from "@/api/device/terminal";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
|
const runtimeFlow = useVueFlow("runtime-flow");
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
const instId = queryParams.get("instId") || "";
|
||||||
|
const taskId = queryParams.get("taskId") || "";
|
||||||
|
const itemId = queryParams.get("itemId") || "03e0842c2fd7ab7575636a473aeefc89";
|
||||||
|
|
||||||
|
const instStatus = ref("RUNNING");
|
||||||
|
const stateMap = {
|
||||||
|
SUCCESS: "已完成",
|
||||||
|
RUNNING: "执行中",
|
||||||
|
FAILED: "失败",
|
||||||
|
PAUSED: "已暂停",
|
||||||
|
STOPPED: "已终止",
|
||||||
|
};
|
||||||
|
let pollTimer = null;
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
const query = () => ({ instId, taskId, itemId });
|
||||||
|
const getProperties = (id) => runtimeFlow.findNode(id)?.data?.properties || {};
|
||||||
|
const setProperties = (id, patch) => {
|
||||||
|
const node = runtimeFlow.findNode(id);
|
||||||
|
if (!node) return;
|
||||||
|
runtimeFlow.updateNodeData(id, {
|
||||||
|
...node.data,
|
||||||
|
properties: { ...node.data.properties, ...patch },
|
||||||
|
}, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const schedulePoll = () => {
|
||||||
|
clearTimeout(pollTimer);
|
||||||
|
pollTimer = window.setTimeout(getLog, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLog = async () => {
|
||||||
|
const res = await getFlowRuntimeLog(query());
|
||||||
|
if (res.code !== 200) return;
|
||||||
|
const records = (res.data || []).filter((item) => item.nodeType);
|
||||||
|
if (!records.length) {
|
||||||
|
if (["RUNNING", "PAUSED"].includes(instStatus.value)) schedulePoll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const states = new Map();
|
||||||
|
records.forEach((item) => {
|
||||||
|
if (["inject", "start"].includes(item.nodeType)) {
|
||||||
|
states.set(item.nodeId, { state: "success", nodeType: item.nodeType, input: item.params });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = states.get(item.nodeId) || { nodeType: item.nodeType };
|
||||||
|
if (item.paramsType === "INPUT") {
|
||||||
|
current.state = item.instStatus.toLowerCase();
|
||||||
|
current.input = item.instStatus === "FAILED" ? item.params : item.message;
|
||||||
|
} else {
|
||||||
|
current.state = item.instStatus === "RUNNING" ? "success" : item.instStatus.toLowerCase();
|
||||||
|
current.output = item.instStatus === "FAILED" ? item.message : item.params;
|
||||||
|
}
|
||||||
|
states.set(item.nodeId, current);
|
||||||
|
});
|
||||||
|
|
||||||
|
states.forEach((value, id) => setProperties(id, value));
|
||||||
|
updateDerivedNodeStates(runtimeFlow.getNodes.value, getProperties, setProperties);
|
||||||
|
|
||||||
|
const last = records.at(-1);
|
||||||
|
instStatus.value = last.instStatus || instStatus.value;
|
||||||
|
if (last.nodeType !== "end" && !["FAILED", "STOPPED", "SUCCESS"].includes(last.instStatus)) schedulePoll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFlow = async () => {
|
||||||
|
if (initialized) return;
|
||||||
|
initialized = true;
|
||||||
|
const { nodes, edges } = await convertNodeRedToVueFlow(itemId);
|
||||||
|
runtimeFlow.setNodes(nodes);
|
||||||
|
runtimeFlow.setEdges(edges);
|
||||||
|
await nextTick();
|
||||||
|
await runtimeFlow.fitView({ padding: 0.16, duration: 300 });
|
||||||
|
await getLog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstDetail = async () => {
|
||||||
|
const res = await getFlowRuntimeLog(query());
|
||||||
|
const last = res.code === 200 ? (res.data || []).at(-1) : null;
|
||||||
|
if (last?.instStatus) instStatus.value = last.instStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
const runControl = async (request, successMessage) => {
|
||||||
|
const res = await request(instId);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(successMessage);
|
||||||
|
await getInstDetail();
|
||||||
|
if (["RUNNING", "PAUSED"].includes(instStatus.value)) schedulePoll();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopFlow = () => runControl(stopInst, "停止成功");
|
||||||
|
const pauseFlow = () => runControl(pauseInst, "暂停成功");
|
||||||
|
const restartFlow = () => runControl(resumeInst, "恢复成功");
|
||||||
|
const fitGraph = () => runtimeFlow.fitView({ padding: 0.16, duration: 300 });
|
||||||
|
const centerView = () => runtimeFlow.fitView({ padding: 0.3, duration: 300 });
|
||||||
|
const closeNodeDetails = () => {
|
||||||
|
const nowTime = Date.now();
|
||||||
|
runtimeFlow.getNodes.value.forEach((node) => setProperties(node.id, { nowTime }));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getInstDetail);
|
||||||
|
onUnmounted(() => clearTimeout(pollTimer));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.runtime-flow-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 620px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #e1e6ee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.1);
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) { margin-left: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-state {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #5a6472;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
.RUNNING, .PAUSED { color: #d48806; }
|
||||||
|
.SUCCESS { color: #2b8a3e; }
|
||||||
|
.FAILED, .STOPPED { color: #c92a2a; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -86,7 +86,7 @@ const showState = ref('NORMAL')
|
|||||||
|
|
||||||
watch(() => props.state, (newVal) => {
|
watch(() => props.state, (newVal) => {
|
||||||
showState.value = newVal;
|
showState.value = newVal;
|
||||||
});
|
}, { immediate: true });
|
||||||
|
|
||||||
const closePopover = () => {
|
const closePopover = () => {
|
||||||
showState.value = 'NORMAL';
|
showState.value = 'NORMAL';
|
||||||
|
|||||||
@ -150,14 +150,14 @@ const execute = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInputFocus = (e) => {
|
const handleInputFocus = (e) => {
|
||||||
// 阻止事件冒泡到 LogicFlow
|
// 阻止事件冒泡到流程画布
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInputKeydown = (e) => {
|
const handleInputKeydown = (e) => {
|
||||||
// 阻止事件冒泡到 Logic Flow 节点,避免被其事件拦截
|
// 阻止事件冒泡到流程节点,避免被画布快捷键拦截
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 可选:明确放行 Ctrl+V(增强兼容性)
|
// 可选:明确放行 Ctrl+V(增强兼容性)
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
import { Group, SelectionSelect } from "@logicflow/extension";
|
|
||||||
import { registerCommon } from "./nodes/common/index";
|
|
||||||
|
|
||||||
import { registerFunction } from './nodes/function'
|
|
||||||
|
|
||||||
import cameraSvg from './icon/camera.svg'
|
import cameraSvg from './icon/camera.svg'
|
||||||
import videoSvg from './icon/video.svg'
|
import videoSvg from './icon/video.svg'
|
||||||
import loopSvg from './icon/loop.svg'
|
import loopSvg from './icon/loop.svg'
|
||||||
@ -19,66 +14,6 @@ import audioSvg from './icon/audio.svg'
|
|||||||
import touchSvg from './icon/touch.svg'
|
import touchSvg from './icon/touch.svg'
|
||||||
import expressionSvg from './icon/expression.svg'
|
import expressionSvg from './icon/expression.svg'
|
||||||
|
|
||||||
import { v4 as randomUUID } from 'uuid'
|
|
||||||
|
|
||||||
export const lfConfig = {
|
|
||||||
idGenerator: () => {
|
|
||||||
// 生成标准UUID并移除连字符
|
|
||||||
// return crypto.randomUUID().replace(/-/g, '');
|
|
||||||
return randomUUID().replace(/-/g, '')
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
backgroundColor: "#f6f8fa",
|
|
||||||
},
|
|
||||||
grid: false,
|
|
||||||
plugins: [Group, SelectionSelect],
|
|
||||||
animation: true,
|
|
||||||
adjustEdgeStartAndEnd: false,
|
|
||||||
// multipleSelectKey: "ctrl",
|
|
||||||
// disabledTools: ["multipleSelect"],
|
|
||||||
partial: true,
|
|
||||||
// group: {
|
|
||||||
// foldable: true, // 启用折叠功能
|
|
||||||
// foldSize: 30, // 折叠后显示的图标尺寸
|
|
||||||
// },
|
|
||||||
edgeType: "bezier",
|
|
||||||
zoomConfig: {
|
|
||||||
min: 0.05, // 最小缩放比例(支持更小值,如 0.01,但不建议过小)
|
|
||||||
max: 3, // 最大缩放比例(可自定义)
|
|
||||||
step: 0.1 // 每次滚轮缩放的步长(默认 0.1)
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
anchor: {
|
|
||||||
show: true, // 强制全局锚点显示
|
|
||||||
hoverOn: false,
|
|
||||||
visibility: "visible",
|
|
||||||
fill: "#FF5722", // 实心绿色
|
|
||||||
stroke: "#FF5722", // 边框同色
|
|
||||||
strokeWidth: 0, // 消除边框
|
|
||||||
r: 7, // 锚点半径
|
|
||||||
hover: { fill: "#FF5722" }, // 禁用悬停变色
|
|
||||||
},
|
|
||||||
anchorLine: {
|
|
||||||
stroke: "#FF5722",
|
|
||||||
strokeWidth: 2,
|
|
||||||
strokeDasharray: "3,2",
|
|
||||||
},
|
|
||||||
bezier: {
|
|
||||||
stroke: "#FF5722",
|
|
||||||
strokeWidth: 2,
|
|
||||||
arrow: false, // 关闭箭头显示
|
|
||||||
animation: true, // 开启动画效果
|
|
||||||
animationSpeed: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 注册自定义节点
|
|
||||||
export const registerCustomizeNode = (lf) => {
|
|
||||||
registerCommon(lf)
|
|
||||||
registerFunction(lf)
|
|
||||||
};
|
|
||||||
|
|
||||||
const deviceOptions = () => {
|
const deviceOptions = () => {
|
||||||
return [{
|
return [{
|
||||||
value: 'cam1',
|
value: 'cam1',
|
||||||
|
|||||||
@ -71,12 +71,45 @@
|
|||||||
<Aside />
|
<Aside />
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main>
|
<el-main>
|
||||||
<div
|
<VueFlow
|
||||||
|
id="main-flow"
|
||||||
class="flow__container"
|
class="flow__container"
|
||||||
ref="flowContainerRef"
|
ref="flowContainerRef"
|
||||||
|
:min-zoom="0.1"
|
||||||
|
:max-zoom="3"
|
||||||
|
:node-drag-threshold="0"
|
||||||
|
:nodes-draggable="!flowStore.disableForm"
|
||||||
|
:nodes-connectable="!flowStore.disableForm"
|
||||||
|
:edges-updatable="!flowStore.disableForm"
|
||||||
|
:elements-selectable="true"
|
||||||
|
:delete-key-code="null"
|
||||||
|
:multi-selection-key-code="['Control', 'Meta']"
|
||||||
|
:selection-key-code="groupSelectionPending ? true : 'Shift'"
|
||||||
|
:pan-on-drag="!groupSelectionPending"
|
||||||
|
:zoom-on-double-click="false"
|
||||||
|
:default-edge-options="defaultEdgeOptions"
|
||||||
|
@connect="handleConnect"
|
||||||
|
@node-double-click="handleNodeDoubleClick"
|
||||||
|
@node-drag-start="handleNodeDragStart"
|
||||||
|
@node-drag="handleNodeDrag"
|
||||||
|
@node-drag-stop="handleNodeDragStop"
|
||||||
|
@nodes-change="handleNodesChange"
|
||||||
|
@edges-change="handleEdgesChange"
|
||||||
|
@edge-click="handleEdgeClick"
|
||||||
|
@pane-click="handlePaneClick"
|
||||||
|
@selection-end="handleSelectionEnd"
|
||||||
@dragover="dragover"
|
@dragover="dragover"
|
||||||
@drop="drop"
|
@drop="drop"
|
||||||
></div>
|
>
|
||||||
|
<template #node-flow-node="nodeProps">
|
||||||
|
<FlowNode v-bind="nodeProps" />
|
||||||
|
</template>
|
||||||
|
<template #edge-flow-edge="edgeProps">
|
||||||
|
<FlowEdge v-bind="edgeProps" />
|
||||||
|
</template>
|
||||||
|
<Background color="#d9dee8" :gap="24" :size="1" />
|
||||||
|
<Controls position="bottom-right" />
|
||||||
|
</VueFlow>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
@ -170,12 +203,16 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
import { computed, reactive, ref, onMounted, onUnmounted, nextTick } from "vue";
|
||||||
import LogicFlow from "@logicflow/core";
|
import { MarkerType, VueFlow, useVueFlow } from "@vue-flow/core";
|
||||||
import "@logicflow/core/es/index.css";
|
import { Background } from "@vue-flow/background";
|
||||||
import "@logicflow/core/lib/style/index.css";
|
import { Controls } from "@vue-flow/controls";
|
||||||
import "@logicflow/extension/lib/style/index.css";
|
import "@vue-flow/core/dist/style.css";
|
||||||
import { lfConfig, registerCustomizeNode } from "./config";
|
import "@vue-flow/core/dist/theme-default.css";
|
||||||
|
import "@vue-flow/controls/dist/style.css";
|
||||||
|
import FlowNode from "./vue-flow/FlowNode.vue";
|
||||||
|
import FlowEdge from "./vue-flow/FlowEdge.vue";
|
||||||
|
import { createFlowFacade } from "./vue-flow/adapter";
|
||||||
import TestRun from "./components/TestRun.vue";
|
import TestRun from "./components/TestRun.vue";
|
||||||
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
||||||
import Aside from "./components/Aside.vue";
|
import Aside from "./components/Aside.vue";
|
||||||
@ -196,6 +233,26 @@ import { Location } from "@element-plus/icons-vue";
|
|||||||
|
|
||||||
const flowContainerRef = ref(null);
|
const flowContainerRef = ref(null);
|
||||||
const lfRef = ref(null);
|
const lfRef = ref(null);
|
||||||
|
const vueFlow = useVueFlow("main-flow");
|
||||||
|
const lf = createFlowFacade(vueFlow);
|
||||||
|
const loopChildDragContexts = new Map();
|
||||||
|
window.lf = lf;
|
||||||
|
|
||||||
|
const defaultEdgeOptions = {
|
||||||
|
type: "flow-edge",
|
||||||
|
animated: true,
|
||||||
|
selectable: true,
|
||||||
|
focusable: true,
|
||||||
|
deletable: true,
|
||||||
|
interactionWidth: 32,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: "#ff6b35",
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
style: { stroke: "#ff6b35", strokeWidth: 2 },
|
||||||
|
};
|
||||||
|
|
||||||
const dragover = (e) => {
|
const dragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -203,12 +260,14 @@ const dragover = (e) => {
|
|||||||
|
|
||||||
const drop = (e) => {
|
const drop = (e) => {
|
||||||
let node = e.dataTransfer.getData("flowNode");
|
let node = e.dataTransfer.getData("flowNode");
|
||||||
|
if (!node) return;
|
||||||
node = JSON.parse(node);
|
node = JSON.parse(node);
|
||||||
if (["stopLoop", "subEnd", "subStart", "currentLoop"].includes(node.type)) {
|
const point = lf.getPointByClient(e.clientX, e.clientY);
|
||||||
|
const loopNode = findLoopAtPoint(point.canvasOverlayPosition);
|
||||||
|
if (["stopLoop", "subEnd", "subStart", "currentLoop"].includes(node.type) && !loopNode) {
|
||||||
ElMessage.warning("此节点只能拖拽到循环节点的循环体中");
|
ElMessage.warning("此节点只能拖拽到循环节点的循环体中");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const point = lf.getPointByClient(e.clientX, e.clientY);
|
|
||||||
const { type, ...other } = node;
|
const { type, ...other } = node;
|
||||||
const newNode = lf.addNode({
|
const newNode = lf.addNode({
|
||||||
type,
|
type,
|
||||||
@ -218,6 +277,9 @@ const drop = (e) => {
|
|||||||
...other,
|
...other,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
|
||||||
|
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
||||||
@ -227,6 +289,112 @@ const drop = (e) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConnect = (connection) => {
|
||||||
|
if (!connection.source || !connection.target) return;
|
||||||
|
const edge = lf.addEdge({
|
||||||
|
type: "bezier",
|
||||||
|
sourceNodeId: connection.source,
|
||||||
|
targetNodeId: connection.target,
|
||||||
|
sourceAnchorId: connection.sourceHandle || `${connection.source}_1`,
|
||||||
|
targetAnchorId: connection.targetHandle || `${connection.target}_3`,
|
||||||
|
});
|
||||||
|
lf.emit("edge:add", { data: edge });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeDoubleClick = ({ node }) => {
|
||||||
|
const data = lf.getGraphData().nodes.find((item) => item.id === node.id);
|
||||||
|
if (!data || ["selectArea", "branch", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)) return;
|
||||||
|
showParamsDrawer.value = true;
|
||||||
|
paramsDrawerData.value = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLoopAtPoint = (point) => {
|
||||||
|
return (vueFlow.getNodes.value || []).find((candidate) => {
|
||||||
|
if (candidate.data?.flowType !== "loop") return false;
|
||||||
|
const groupPosition = candidate.computedPosition || candidate.position;
|
||||||
|
const groupWidth = Number(candidate.dimensions?.width || candidate.data?.size?.width || 0);
|
||||||
|
const groupHeight = Number(candidate.dimensions?.height || candidate.data?.size?.height || 0);
|
||||||
|
return point.x >= groupPosition.x + 24
|
||||||
|
&& point.x <= groupPosition.x + groupWidth - 24
|
||||||
|
&& point.y >= groupPosition.y + 112
|
||||||
|
&& point.y <= groupPosition.y + groupHeight - 24;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findContainingLoop = (node) => {
|
||||||
|
const position = node.computedPosition || node.position;
|
||||||
|
const width = Number(node.dimensions?.width || node.data?.size?.width || 0);
|
||||||
|
const height = Number(node.dimensions?.height || node.data?.size?.height || 0);
|
||||||
|
return findLoopAtPoint({ x: position.x + width / 2, y: position.y + height / 2 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventClientPoint = (event) => {
|
||||||
|
const pointer = event?.touches?.[0] || event?.changedTouches?.[0] || event;
|
||||||
|
if (!Number.isFinite(pointer?.clientX) || !Number.isFinite(pointer?.clientY)) return null;
|
||||||
|
return { x: pointer.clientX, y: pointer.clientY };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeDragStart = ({ node, event }) => {
|
||||||
|
const drag = lf.prepareLoopChildDrag(node.id);
|
||||||
|
const clientPoint = eventClientPoint(event);
|
||||||
|
if (!drag || !clientPoint) return;
|
||||||
|
const pointer = lf.getPointByClient(clientPoint.x, clientPoint.y).canvasOverlayPosition;
|
||||||
|
loopChildDragContexts.set(node.id, {
|
||||||
|
groupId: drag.groupId,
|
||||||
|
offset: { x: pointer.x - drag.position.x, y: pointer.y - drag.position.y },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLoopChildDrag = ({ node, event }) => {
|
||||||
|
const drag = loopChildDragContexts.get(node.id);
|
||||||
|
const clientPoint = eventClientPoint(event);
|
||||||
|
if (!drag || !clientPoint) return;
|
||||||
|
const pointer = lf.getPointByClient(clientPoint.x, clientPoint.y).canvasOverlayPosition;
|
||||||
|
lf.layoutLoopDuringDrag(drag.groupId, node.id, {
|
||||||
|
x: pointer.x - drag.offset.x,
|
||||||
|
y: pointer.y - drag.offset.y,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeDrag = (payload) => updateLoopChildDrag(payload);
|
||||||
|
|
||||||
|
const handleNodeDragStop = (payload) => {
|
||||||
|
const { node } = payload;
|
||||||
|
updateLoopChildDrag(payload);
|
||||||
|
loopChildDragContexts.delete(node.id);
|
||||||
|
const vueNode = vueFlow.findNode(node.id);
|
||||||
|
if (
|
||||||
|
vueNode
|
||||||
|
&& !vueNode.parentNode
|
||||||
|
&& !["start", "end", "loop", "selectArea"].includes(vueNode.data?.flowType)
|
||||||
|
) {
|
||||||
|
const loopNode = findContainingLoop(vueNode);
|
||||||
|
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const graph = lf.getGraphData();
|
||||||
|
const groups = recursiveFilter(graph.nodes, node.id, ["loop", "selectArea"]);
|
||||||
|
operationEdge(groups);
|
||||||
|
lf.markChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodesChange = (changes) => {
|
||||||
|
if (changes.some((change) => ["add", "remove"].includes(change.type) || (change.type === "position" && change.dragging === false))) {
|
||||||
|
lf.markChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdgesChange = (changes) => {
|
||||||
|
if (changes.some((change) => ["add", "remove"].includes(change.type))) lf.markChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdgeClick = ({ event, edge }) => {
|
||||||
|
event?.stopPropagation?.();
|
||||||
|
lf.selectElementById(edge.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaneClick = () => lf.clearSelectElements();
|
||||||
|
|
||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
flowStore.getDeviceList();
|
flowStore.getDeviceList();
|
||||||
|
|
||||||
@ -525,170 +693,77 @@ const justLoadFlow = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let flowInitialized = false;
|
||||||
const initFlow = () => {
|
const initFlow = () => {
|
||||||
if (flowContainerRef.value) {
|
if (flowInitialized) return;
|
||||||
const lf = new LogicFlow({
|
flowInitialized = true;
|
||||||
container: flowContainerRef.value,
|
lfRef.value = lf;
|
||||||
...lfConfig,
|
loadFlowData(flowInfoData.value.itemId, lf);
|
||||||
zoomOptions: {
|
|
||||||
initScale: 0.5, // 初始缩放 50%
|
|
||||||
minScale: 0.1, // 最小缩放 10%
|
|
||||||
maxScale: 3, // 最大缩放 300%
|
|
||||||
},
|
|
||||||
keyboard: {
|
|
||||||
enabled: true,
|
|
||||||
shortcuts: [
|
|
||||||
{
|
|
||||||
keys: ["backspace", "delete"], // 覆盖删除键
|
|
||||||
callback: () => {
|
|
||||||
const activeElem = document.activeElement;
|
|
||||||
if (["INPUT", "TEXTAREA"].includes(activeElem.tagName)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const { nodes, edges } = lf.getSelectElements();
|
lf.on("edge:add", ({ data }) => validEdge(data));
|
||||||
if (nodes.length > 0) {
|
lf.on("history:change", () => {
|
||||||
// 检查是否存在受保护节点
|
if (flowStore.disableForm) return;
|
||||||
const hasProtectedNode = nodes.some((node) =>
|
const flowData = lf.getGraphData();
|
||||||
["start", "end"].includes(node.type)
|
if (JSON.stringify(oldFlowData) !== JSON.stringify(flowData)) {
|
||||||
);
|
|
||||||
|
|
||||||
if (hasProtectedNode) {
|
|
||||||
lf.selectElementById(null); // 取消选中
|
|
||||||
return false; // 阻断删除
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认删除逻辑
|
|
||||||
nodes.forEach((node) => {
|
|
||||||
if (node.type === "selectArea") {
|
|
||||||
lf.getNodeModelById(node.id).children = [];
|
|
||||||
node.children.forEach((item) => {
|
|
||||||
lf.getNodeModelById(item).draggable = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lf.deleteNode(node.id);
|
|
||||||
// 删除节点所有的边
|
|
||||||
lf.deleteEdgeByNodeId(node.id);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (nodes.length === 0 && edges.length > 0) {
|
|
||||||
edges.forEach((node) => {
|
|
||||||
// 删除边
|
|
||||||
lf.deleteEdge(node.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["ctrl+z"], // 自定义快捷键
|
|
||||||
callback: () => {
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: ["ctrl+shift+z"],
|
|
||||||
callback: () => true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
registerCustomizeNode(lf);
|
|
||||||
|
|
||||||
loadFlowData(flowInfoData.value.itemId, lf);
|
|
||||||
|
|
||||||
lf.on("edge:add", ({ data }) => {
|
|
||||||
validEdge(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听历史记录变化
|
|
||||||
lf.on("history:change", ({ data }) => {
|
|
||||||
if (flowStore.disableForm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const flowData = lf.getGraphData();
|
|
||||||
if (JSON.stringify(oldFlowData) === JSON.stringify(flowData)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
flowState.value = "updateData";
|
flowState.value = "updateData";
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 监听子节点拖动事件
|
const groupSelectionPending = ref(false);
|
||||||
lf.on("node:mousemove", ({ data, e }) => {
|
|
||||||
const { nodes } = lf.getGraphData();
|
|
||||||
const arr = recursiveFilter(nodes, data.id, "loop");
|
|
||||||
arr.forEach((item) => {
|
|
||||||
const node = lf.getNodeModelById(item.id);
|
|
||||||
node.updateSize();
|
|
||||||
});
|
|
||||||
operationEdge(arr);
|
|
||||||
});
|
|
||||||
|
|
||||||
lf.on("node:mouseleave", ({ data }) => {
|
const createGroup = (selectedNodes) => {
|
||||||
const node = lf.getNodeModelById(data.id);
|
const nodes = selectedNodes.filter((item) => !["selectArea", "loop"].includes(item.type));
|
||||||
if (node.setCustomProperties) {
|
if (!nodes.length) {
|
||||||
node.setCustomProperties();
|
groupSelectionPending.value = false;
|
||||||
}
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
lf.on("node:dbclick", ({ data, e }) => {
|
|
||||||
if (
|
|
||||||
["selectArea", "branch", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showParamsDrawer.value = true;
|
|
||||||
paramsDrawerData.value = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
lfRef.value = lf;
|
|
||||||
window.lf = lf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PADDING = 200;
|
||||||
|
let minX = Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
const children = [];
|
||||||
|
|
||||||
|
nodes.forEach((item) => {
|
||||||
|
const model = lf.getNodeModelById(item.id);
|
||||||
|
const width = model?.width || 372;
|
||||||
|
const height = model?.height || 180;
|
||||||
|
minX = Math.min(minX, item.x - width / 2);
|
||||||
|
minY = Math.min(minY, item.y - height / 2);
|
||||||
|
maxX = Math.max(maxX, item.x + width / 2);
|
||||||
|
maxY = Math.max(maxY, item.y + height / 2);
|
||||||
|
children.push(item.id);
|
||||||
|
if (model) model.draggable = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
lf.addNode({
|
||||||
|
type: "selectArea",
|
||||||
|
x: minX + (maxX - minX) / 2,
|
||||||
|
y: minY + (maxY - minY) / 2,
|
||||||
|
properties: {
|
||||||
|
name: "分组",
|
||||||
|
action: "NONE",
|
||||||
|
width: maxX - minX + PADDING,
|
||||||
|
height: maxY - minY + PADDING,
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
lf.clearSelectElements();
|
||||||
|
groupSelectionPending.value = false;
|
||||||
|
nextTick(() => lf.fitView());
|
||||||
};
|
};
|
||||||
|
|
||||||
const group = () => {
|
const group = () => {
|
||||||
const PADDING = 200;
|
const { nodes } = lf.getSelectElements();
|
||||||
lf.extension.selectionSelect.openSelectionSelect();
|
if (nodes.length) createGroup(nodes);
|
||||||
lf.once("selection:selected", (data) => {
|
else groupSelectionPending.value = true;
|
||||||
let minX = Infinity;
|
};
|
||||||
let minY = Infinity;
|
|
||||||
let maxX = -Infinity;
|
|
||||||
let maxY = -Infinity;
|
|
||||||
|
|
||||||
let children = [];
|
const handleSelectionEnd = () => {
|
||||||
data.elements.forEach((item) => {
|
if (!groupSelectionPending.value) return;
|
||||||
if (item.BaseType === "node") {
|
createGroup(lf.getSelectElements().nodes);
|
||||||
minX = Math.min(minX, item.x - item.width / 2);
|
|
||||||
minY = Math.min(minY, item.y - item.height / 2);
|
|
||||||
maxX = Math.max(maxX, item.x + item.width / 2);
|
|
||||||
maxY = Math.max(maxY, item.y + item.height / 2);
|
|
||||||
children.push(item.id);
|
|
||||||
lf.getNodeModelById(item.id).draggable = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (children.length === 0) {
|
|
||||||
lf.clearSelectElements();
|
|
||||||
lf.extension.selectionSelect.closeSelectionSelect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let width = maxX - minX + PADDING;
|
|
||||||
let height = maxY - minY + PADDING;
|
|
||||||
|
|
||||||
lf.addNode({
|
|
||||||
type: "selectArea",
|
|
||||||
x: minX + (maxX - minX) / 2,
|
|
||||||
y: minY + (maxY - minY) / 2,
|
|
||||||
properties: {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
},
|
|
||||||
children,
|
|
||||||
});
|
|
||||||
lf.clearSelectElements();
|
|
||||||
lf.extension.selectionSelect.closeSelectionSelect();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const treeData = ref(null);
|
const treeData = ref(null);
|
||||||
@ -774,6 +849,48 @@ const clearRun = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleDeleteKey = (event) => {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (["INPUT", "TEXTAREA"].includes(activeElement?.tagName) || activeElement?.isContentEditable) return;
|
||||||
|
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
|
||||||
|
if (flowStore.disableForm) return;
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.shiftKey) lf.redo();
|
||||||
|
else lf.undo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["Backspace", "Delete"].includes(event.key) || flowStore.disableForm) return;
|
||||||
|
|
||||||
|
const { nodes, edges } = lf.getSelectElements();
|
||||||
|
if (nodes.some((node) => ["start", "end"].includes(node.type))) {
|
||||||
|
lf.clearSelectElements();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodes.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
if (node.type === "selectArea") {
|
||||||
|
const model = lf.getNodeModelById(node.id);
|
||||||
|
const children = Array.from(model?.children || []);
|
||||||
|
model.children = [];
|
||||||
|
children.forEach((childId) => {
|
||||||
|
const child = lf.getNodeModelById(childId);
|
||||||
|
if (child) child.draggable = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lf.deleteNode(node.id);
|
||||||
|
emitter.emit("deleteNode", node.id);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (edges.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
edges.forEach((edge) => lf.deleteEdge(edge.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const nodeErrorList = ref([]);
|
const nodeErrorList = ref([]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -781,6 +898,7 @@ onMounted(() => {
|
|||||||
initFlow();
|
initFlow();
|
||||||
document.addEventListener("singleNodeExecution", singleNodeExecution);
|
document.addEventListener("singleNodeExecution", singleNodeExecution);
|
||||||
registerBeforeUnload();
|
registerBeforeUnload();
|
||||||
|
window.addEventListener("keydown", handleDeleteKey);
|
||||||
|
|
||||||
emitter.on("openParamsDrawer", (node) => {
|
emitter.on("openParamsDrawer", (node) => {
|
||||||
showParamsDrawer.value = true;
|
showParamsDrawer.value = true;
|
||||||
@ -809,11 +927,11 @@ onMounted(() => {
|
|||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (!nodeErrorList.value.includes(id)) {
|
if (!nodeErrorList.value.includes(id)) {
|
||||||
nodeErrorList.value.push(id);
|
nodeErrorList.value.push(id);
|
||||||
element.style.background = "#ff00001a";
|
if (element) element.style.background = "#ff00001a";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nodeErrorList.value = nodeErrorList.value.filter((item) => item !== id);
|
nodeErrorList.value = nodeErrorList.value.filter((item) => item !== id);
|
||||||
element.style.background = "#fff";
|
if (element) element.style.background = "#fff";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -832,16 +950,20 @@ const operationEdge = (arr) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
loopChildDragContexts.clear();
|
||||||
emitter.off("changeNodeState");
|
emitter.off("changeNodeState");
|
||||||
emitter.off("openParamsDrawer");
|
emitter.off("openParamsDrawer");
|
||||||
emitter.off("throwAnError");
|
emitter.off("throwAnError");
|
||||||
emitter.off("deleteNode");
|
emitter.off("deleteNode");
|
||||||
|
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
window.removeEventListener("keydown", handleDeleteKey);
|
||||||
document.removeEventListener("singleNodeExecution", singleNodeExecution);
|
document.removeEventListener("singleNodeExecution", singleNodeExecution);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
$flow-canvas-cursor: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2724%27%20height=%2724%27%20viewBox=%270%200%2024%2024%27%3E%3Cpath%20d=%27M4%202.5v16l4.2-4%203.1%207.1%203.4-1.5-3.1-7.1h5.8z%27%20fill=%27%23fff%27%20stroke=%27%231f2937%27%20stroke-width=%271.8%27%20stroke-linejoin=%27round%27/%3E%3C/svg%3E") 4 3, default;
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -856,7 +978,9 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #ddd;
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #e3e7ee;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
.btn__container {
|
.btn__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -879,39 +1003,68 @@ onUnmounted(() => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 260px;
|
width: 260px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border-right: 1px solid #e3e7ee;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow__container {
|
.flow__container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
cursor: $flow-canvas-cursor;
|
||||||
|
|
||||||
:deep(.lf-base) {
|
:deep(.vue-flow__node) {
|
||||||
foreignObject {
|
border: 0;
|
||||||
overflow: visible !important;
|
background: transparent;
|
||||||
}
|
cursor: $flow-canvas-cursor;
|
||||||
|
}
|
||||||
|
|
||||||
.lf-node-content {
|
:deep(.vue-flow__node .flow-node-shell),
|
||||||
.lf-node-anchor {
|
:deep(.vue-flow__node .flow-node-content),
|
||||||
display: block !important; /* 强制显示 */
|
:deep(.vue-flow__node .node__container),
|
||||||
opacity: 1 !important; /* 取消透明度 */
|
:deep(.vue-flow__node .right),
|
||||||
}
|
:deep(.vue-flow__node .editIcon),
|
||||||
}
|
:deep(.vue-flow__node .resultText) {
|
||||||
|
cursor: $flow-canvas-cursor;
|
||||||
|
}
|
||||||
|
|
||||||
.lf-node-selected {
|
:deep(.vue-flow__edge-path),
|
||||||
.node__box {
|
:deep(.vue-flow__connection-path) {
|
||||||
border-color: #8dc99d;
|
stroke: #ff6b35;
|
||||||
}
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.node__container {
|
:deep(.vue-flow__pane) {
|
||||||
border-color: #8dc99d;
|
cursor: $flow-canvas-cursor;
|
||||||
// pointer-events: none;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.group__container {
|
:deep(.vue-flow__pane.dragging) {
|
||||||
border-color: #8dc99d;
|
cursor: $flow-canvas-cursor;
|
||||||
// pointer-events: none;
|
}
|
||||||
}
|
|
||||||
}
|
:deep(.vue-flow__handle) {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge-interaction) {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
|
||||||
|
stroke: #1677ff;
|
||||||
|
stroke-width: 4;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(22, 119, 255, 0.45));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.vue-flow__controls) {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #dfe4ec;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 6px 18px rgba(20, 33, 61, 0.12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,196 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import EndNode from "./endNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class EndNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(EndNode, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
text: props.model.text.value,
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.use(JsonViewer)
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class EndNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证表单
|
|
||||||
async validateForm() {
|
|
||||||
return { valid: true, message: "无验证方法" };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y - 25,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerEndNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "end",
|
|
||||||
view: EndNodeHtmlNode,
|
|
||||||
model: EndNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { registerStartNode } from "./startNode";
|
|
||||||
import { registerEndNode } from "./endNode";
|
|
||||||
import { registerSelectAreaNode } from './selectAreaNode.js'
|
|
||||||
import { registerServiceNode } from './serviceNode.js'
|
|
||||||
|
|
||||||
export const registerCommon = (lf) => {
|
|
||||||
registerStartNode(lf)
|
|
||||||
registerEndNode(lf)
|
|
||||||
registerSelectAreaNode(lf)
|
|
||||||
registerServiceNode(lf)
|
|
||||||
}
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { GroupNodeModel } from '@logicflow/extension';
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import { HtmlNode, h as svgH } from "@logicflow/core";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// @ts-ignore
|
|
||||||
import SelectAreaNode from './selectAreaNode.vue';
|
|
||||||
|
|
||||||
class SelectAreaModel extends GroupNodeModel {
|
|
||||||
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.children = new Set(data.children || [])
|
|
||||||
this.zIndex = 0;
|
|
||||||
this.isGroup = true;
|
|
||||||
this.resizable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultAnchor() {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
isAllowAppendIn() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SelectAreaView extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(SelectAreaNode, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (SelectAreaView.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(SelectAreaView.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.width = "100%"; // 关键:确保容器自适应内容
|
|
||||||
this.container.style.height = "100%"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(this.container);
|
|
||||||
SelectAreaView.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSelectAreaNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "selectArea",
|
|
||||||
view: SelectAreaView,
|
|
||||||
model: SelectAreaModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import ServiceNode from "./serviceNode.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class ServiceNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: ServiceNode,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (ServiceNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(ServiceNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
ServiceNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class ServiceNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerServiceNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "serviceNode",
|
|
||||||
view: ServiceNodeHtmlNode,
|
|
||||||
model: ServiceNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
<template #output>
|
<template #output>
|
||||||
<div v-if="nodeOperatingStatus != 'FAILED'">
|
<div v-if="nodeOperatingStatus != 'FAILED'">
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
v-if="props.properties.outputType === 'json'"
|
v-if="!['img', 'video'].includes(props.properties.outputType)"
|
||||||
:value="outputJsonData"
|
:value="outputJsonData"
|
||||||
copyable
|
copyable
|
||||||
boxed
|
boxed
|
||||||
|
|||||||
@ -1,204 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from '@logicflow/core';
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from 'vue';
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import StartNode from './startNode.vue';
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class StartNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: StartNode,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties()
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (StartNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(StartNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement('div');
|
|
||||||
this.container.style.display = 'inline-block'; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus) // 关键:单独注册ElementPlus
|
|
||||||
this.app.use(JsonViewer)
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
StartNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize () {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector('.node__box')
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class StartNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
this.deletable = false; // 关键代码:禁止删除节点
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this, x = _a.x, y = _a.y, width = _a.width, height = _a.height;
|
|
||||||
return [
|
|
||||||
{ x: x + width / 2 - 24, y: y - 25, name: 'right', id: "".concat(this.id, "_1"), properties: { connectionType: 'source' }, onlyAsSource: true },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = 'none';
|
|
||||||
// style.hover.stroke = 'none';
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerStartNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "start",
|
|
||||||
view: StartNodeHtmlNode,
|
|
||||||
model: StartNodeHtmlModel,
|
|
||||||
effect: ['status'],
|
|
||||||
events: {
|
|
||||||
remove: (event) => {
|
|
||||||
// 阻止默认的删除事件
|
|
||||||
event.preventDefault();
|
|
||||||
// 可以在此处添加自定义的删除逻辑,例如弹出提示框
|
|
||||||
alert('此节点不可删除');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import CodeNode from "./codeNode.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class CodeNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: CodeNode,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (CodeNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(CodeNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
CodeNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class CodeNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCodeNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "code",
|
|
||||||
view: CodeNodeHtmlNode,
|
|
||||||
model: CodeNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
<template #output>
|
<template #output>
|
||||||
<div v-if="nodeOperatingStatus != 'FAILED'">
|
<div v-if="nodeOperatingStatus != 'FAILED'">
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
v-if="props.properties.outputType === 'json'"
|
v-if="!['img', 'video'].includes(props.properties.outputType)"
|
||||||
:value="outputJsonData"
|
:value="outputJsonData"
|
||||||
copyable
|
copyable
|
||||||
boxed
|
boxed
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
||||||
</template>
|
</template>
|
||||||
<template #output>
|
<template #output>
|
||||||
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer v-if="!['img', 'video'].includes(props.properties.outputType)" :value="outputJsonData" copyable boxed sort theme="light" />
|
||||||
<el-image
|
<el-image
|
||||||
v-if="props.properties.outputType === 'img'"
|
v-if="props.properties.outputType === 'img'"
|
||||||
v-for="item in outputJsonData.imageUrl"
|
v-for="item in outputJsonData.imageUrl"
|
||||||
|
|||||||
@ -1,219 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import CurrentLoop from "./currentLoop.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class CurrentLoopNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: CurrentLoop,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (CurrentLoopNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(CurrentLoopNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
CurrentLoopNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class CurrentLoopNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证表单
|
|
||||||
async validateForm() {
|
|
||||||
const result = this.componentInstance.validateForm()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerCurrentLoopNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "currentLoop",
|
|
||||||
view: CurrentLoopNodeHtmlNode,
|
|
||||||
model: CurrentLoopNodeHtmlModel
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import HttpNode from "./httpNode.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class HttpNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: HttpNode,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (HttpNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(HttpNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
HttpNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class HttpNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerHttpNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "http",
|
|
||||||
view: HttpNodeHtmlNode,
|
|
||||||
model: HttpNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
<template #output>
|
<template #output>
|
||||||
<div v-if="nodeOperatingStatus != 'FAILED'">
|
<div v-if="nodeOperatingStatus != 'FAILED'">
|
||||||
<JsonViewer
|
<JsonViewer
|
||||||
v-if="props.properties.outputType === 'json'"
|
v-if="!['img', 'video'].includes(props.properties.outputType)"
|
||||||
:value="outputJsonData"
|
:value="outputJsonData"
|
||||||
copyable
|
copyable
|
||||||
boxed
|
boxed
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import { registerLoopBodyNode } from './loop'
|
|
||||||
import { registerStopLoopNode } from './stopLoop'
|
|
||||||
import { registerSwitch } from './switch'
|
|
||||||
import { registerSubEndNode } from './subEnd'
|
|
||||||
import { registerSubStartNode } from './subStart'
|
|
||||||
import { registerSleepNode } from './sleep'
|
|
||||||
import { registerHttpNode } from './httpNode'
|
|
||||||
import { registerCodeNode } from './codeNode'
|
|
||||||
import { registerCurrentLoopNode } from './currentLoopNode'
|
|
||||||
import { registerSdAgentNode } from './sdAgent'
|
|
||||||
import { registerRecognizeNode } from './recognize'
|
|
||||||
|
|
||||||
export const registerFunction = (lf) => {
|
|
||||||
registerLoopBodyNode(lf)
|
|
||||||
registerStopLoopNode(lf)
|
|
||||||
registerSwitch(lf)
|
|
||||||
registerSubEndNode(lf)
|
|
||||||
registerSubStartNode(lf)
|
|
||||||
registerSleepNode(lf)
|
|
||||||
registerHttpNode(lf)
|
|
||||||
registerCodeNode(lf)
|
|
||||||
registerCurrentLoopNode(lf)
|
|
||||||
registerSdAgentNode(lf)
|
|
||||||
registerRecognizeNode(lf)
|
|
||||||
}
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
import { GroupNodeModel } from '@logicflow/extension';
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import { HtmlNode, h as svgH } from "@logicflow/core";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
import Loop from './loop.vue';
|
|
||||||
|
|
||||||
class loopBodyModel extends GroupNodeModel {
|
|
||||||
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
this.children = new Set(data.children || [])
|
|
||||||
this.zIndex = 0;
|
|
||||||
this.isGroup = true;
|
|
||||||
this.resizable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归获取所有有效子节点(包含多层嵌套 children)
|
|
||||||
getAllValidChildren = (nodeIds, graphModel) => {
|
|
||||||
const validNodes = [];
|
|
||||||
// 遍历当前层节点 ID
|
|
||||||
Array.from(nodeIds).forEach((id) => {
|
|
||||||
const node = graphModel.getNodeModelById(id);
|
|
||||||
if (node) {
|
|
||||||
// 当前节点有效,加入结果
|
|
||||||
validNodes.push(node);
|
|
||||||
// 如果节点还有 children,递归获取下级节点
|
|
||||||
if (node.children && node.children.size > 0) {
|
|
||||||
const childNodes = this.getAllValidChildren(node.children, graphModel);
|
|
||||||
validNodes.push(...childNodes);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 无效 ID,从集合中删除
|
|
||||||
nodeIds.delete(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return validNodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算子节点的包围盒(包含所有子节点的最小矩形区域)
|
|
||||||
getChildrenBBox() {
|
|
||||||
const children = this.getAllValidChildren(this.children, this.graphModel)
|
|
||||||
// Array.from(this.children).forEach((id) => {
|
|
||||||
// const node = this.graphModel.getNodeModelById(id)
|
|
||||||
// if (node) {
|
|
||||||
// children.push(this.graphModel.getNodeModelById(id))
|
|
||||||
// } else {
|
|
||||||
// this.children.delete(id)
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
||||||
nextTick(() => {
|
|
||||||
children.forEach((node) => {
|
|
||||||
let _width = node.type === 'loop' ? 500 : 320
|
|
||||||
let _height = node.type === 'loop' ? 300 : 200
|
|
||||||
let width = node.width === 0 ? _width : node.width
|
|
||||||
let height = node.height === 0 ? _height : node.height
|
|
||||||
minX = Math.min(minX, node.x - width / 2);
|
|
||||||
maxX = Math.max(maxX, node.x + width / 2);
|
|
||||||
minY = Math.min(minY, node.y - height / 2);
|
|
||||||
maxY = Math.max(maxY, node.y + height / 2);
|
|
||||||
});
|
|
||||||
this.width = maxX - minX + 180; // 增加内边距
|
|
||||||
this.height = maxY - minY + 300;
|
|
||||||
this.x = (minX + maxX) / 2;
|
|
||||||
this.y = (minY + maxY) / 2;
|
|
||||||
const properties = lf.getProperties(this.id)
|
|
||||||
this.setProperties({
|
|
||||||
...properties,
|
|
||||||
width: this.width,
|
|
||||||
height: this.height
|
|
||||||
})
|
|
||||||
this.getDefaultAnchor()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize() {
|
|
||||||
const children = Array.from(this.children);
|
|
||||||
if (children.length > 0) {
|
|
||||||
this.getChildrenBBox();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加子节点时立即更新大小
|
|
||||||
addChild(childId) {
|
|
||||||
super.addChild(childId);
|
|
||||||
this.updateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除子节点时更新大小
|
|
||||||
removeChild(childId) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 ,
|
|
||||||
// y: y + height / 2,
|
|
||||||
y:y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
// y: y + height / 2,
|
|
||||||
y:y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
isAllowAppendIn() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class loopBodyView extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(Loop, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onAddToGroup: this.addChild.bind(this),
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addChild(data) {
|
|
||||||
this.addToGroup(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.props.model.updateSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关键修改:重写getShape方法
|
|
||||||
getShape() {
|
|
||||||
const { model } = this.props;
|
|
||||||
const { x, y, width, height } = model;
|
|
||||||
|
|
||||||
// 创建自定义HTML内容
|
|
||||||
const html = super.getShape();
|
|
||||||
// 创建SVG组,包含HTML内容和锚点
|
|
||||||
return svgH('g', {}, [
|
|
||||||
// 绘制组节点背景
|
|
||||||
svgH('rect', {
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y - height / 2,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
stroke: '#88f',
|
|
||||||
strokeWidth: 0,
|
|
||||||
strokeDasharray: '5,5',
|
|
||||||
fill: 'rgba(136, 136, 255, 0.1)',
|
|
||||||
rx: 4,
|
|
||||||
ry: 4
|
|
||||||
}),
|
|
||||||
|
|
||||||
// 添加HTML内容
|
|
||||||
html,
|
|
||||||
|
|
||||||
// 渲染锚点
|
|
||||||
this.getAnchorShapes()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建锚点元素
|
|
||||||
getAnchorShapes() {
|
|
||||||
const { model } = this.props;
|
|
||||||
const anchors = model.anchors || [];
|
|
||||||
|
|
||||||
const anchorShapes = anchors.map((offset, index) => {
|
|
||||||
const {x, y } = offset
|
|
||||||
|
|
||||||
return svgH('circle', {
|
|
||||||
cx: x,
|
|
||||||
cy: y,
|
|
||||||
r: 7,
|
|
||||||
hover: { fill: "#FF5722" }, // 禁用悬停变色
|
|
||||||
fill: '#FF5722',
|
|
||||||
stroke: '#FF5722',
|
|
||||||
strokeWidth: 0,
|
|
||||||
className: 'lf-node-anchor',
|
|
||||||
cursor: 'crosshair',
|
|
||||||
'data-anchor-id': `anchor_${index}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return svgH('g', {}, anchorShapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (loopBodyView.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(loopBodyView.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
// this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
this.container.style.width = "100%"; // 关键:确保容器自适应内容
|
|
||||||
this.container.style.height = "100%"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(this.container);
|
|
||||||
loopBodyView.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addToGroup(childId) {
|
|
||||||
const groupModel = lf.getNodeModelById(this.props.model.id);
|
|
||||||
groupModel.addChild(childId);
|
|
||||||
groupModel.updateSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerLoopBodyNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "loop",
|
|
||||||
view: loopBodyView,
|
|
||||||
model: loopBodyModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="group__container" :class="props.model.id" @mouseleave="setNodeProperties">
|
<div
|
||||||
|
class="group__container"
|
||||||
|
:class="[props.model.id, { 'group__container--dragover': isDragOver }]"
|
||||||
|
@dragenter.prevent="isDragOver = true"
|
||||||
|
@dragleave="handleDragleave"
|
||||||
|
>
|
||||||
<NodeTitle
|
<NodeTitle
|
||||||
:icon="loop"
|
:icon="loop"
|
||||||
:nodeId="props.model.id"
|
:nodeId="props.model.id"
|
||||||
@ -9,7 +14,10 @@
|
|||||||
:showZoom="false"
|
:showZoom="false"
|
||||||
@setNodeName="setNodeName"
|
@setNodeName="setNodeName"
|
||||||
/>
|
/>
|
||||||
<div>循环体</div>
|
<div class="loop-body-title">
|
||||||
|
<span>循环体</span>
|
||||||
|
<span class="loop-body-count">{{ childCount }} 个节点</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="child__container"
|
class="child__container"
|
||||||
@drop="handleDrop"
|
@drop="handleDrop"
|
||||||
@ -24,7 +32,7 @@ import NodeTitle from "../../components/NodeTitle.vue";
|
|||||||
import loop from "../../icon/loop.svg";
|
import loop from "../../icon/loop.svg";
|
||||||
import { addNewEdge } from "@/utils/flow";
|
import { addNewEdge } from "@/utils/flow";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import { onUnmounted } from "vue";
|
import { computed, onUnmounted, ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
@ -35,17 +43,27 @@ const props = defineProps({
|
|||||||
|
|
||||||
|
|
||||||
const emits = defineEmits(["bindRef", "addToGroup", "contentChange"]);
|
const emits = defineEmits(["bindRef", "addToGroup", "contentChange"]);
|
||||||
|
const isDragOver = ref(false);
|
||||||
|
const childCount = computed(() => props.model?.children?.size || 0);
|
||||||
|
|
||||||
const handleDragover = (e) => {
|
const handleDragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
isDragOver.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragleave = (e) => {
|
||||||
|
if (!e.currentTarget.contains(e.relatedTarget)) isDragOver.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e) => {
|
const handleDrop = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
isDragOver.value = false;
|
||||||
|
|
||||||
let flowNode = e.dataTransfer.getData("flowNode");
|
let flowNode = e.dataTransfer.getData("flowNode");
|
||||||
|
if (!flowNode) return;
|
||||||
flowNode = JSON.parse(flowNode);
|
flowNode = JSON.parse(flowNode);
|
||||||
|
if (["start", "end", "loop", "selectArea"].includes(flowNode.type)) return;
|
||||||
const point = lf.getPointByClient(e.clientX, e.clientY);
|
const point = lf.getPointByClient(e.clientX, e.clientY);
|
||||||
const { type, ...other } = flowNode;
|
const { type, ...other } = flowNode;
|
||||||
const node = lf.addNode({
|
const node = lf.addNode({
|
||||||
@ -63,7 +81,7 @@ const handleDrop = (e) => {
|
|||||||
emitter.emit("openParamsDrawer", node);
|
emitter.emit("openParamsDrawer", node);
|
||||||
}
|
}
|
||||||
|
|
||||||
emits("addToGroup", node.id);
|
lf.addToGroup(props.model.id, node.id, point.canvasOverlayPosition);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
addNewEdge(props.model.id);
|
addNewEdge(props.model.id);
|
||||||
}, 50);
|
}, 50);
|
||||||
@ -87,26 +105,71 @@ onUnmounted(() => {
|
|||||||
.group__container {
|
.group__container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
// min-height: 420px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
border: 2px solid white;
|
border: 1px solid #cfd7e6;
|
||||||
// box-shadow: 0 5px 15px 0#00000008;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 5px;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
background: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dragover {
|
||||||
|
border-color: #ff6b35;
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255, 107, 53, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loop-body-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 28px;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loop-body-count {
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: #667085;
|
||||||
|
background: #eef1f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.child__container {
|
.child__container {
|
||||||
min-height: 100px;
|
min-height: 300px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #f6f8fa;
|
background-color: #f8fafc;
|
||||||
border-radius: 8px;
|
border: 1px dashed #b8c2d2;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.18s ease, border-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--dragover .child__container {
|
||||||
|
background-color: #fff7f2;
|
||||||
|
border-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-row {
|
.el-row {
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import Recognize from "./recognize.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class RecognizeNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: Recognize,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (RecognizeNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(RecognizeNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
RecognizeNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class RecognizeNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerRecognizeNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "recognize",
|
|
||||||
view: RecognizeNodeHtmlNode,
|
|
||||||
model: RecognizeNodeHtmlModel
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
||||||
</template>
|
</template>
|
||||||
<template #output>
|
<template #output>
|
||||||
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer v-if="!['img', 'video'].includes(props.properties.outputType)" :value="outputJsonData" copyable boxed sort theme="light" />
|
||||||
<el-image
|
<el-image
|
||||||
v-if="props.properties.outputType === 'img'"
|
v-if="props.properties.outputType === 'img'"
|
||||||
v-for="item in outputJsonData.imageUrl"
|
v-for="item in outputJsonData.imageUrl"
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import SdAgent from "./sdAgent.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class SdAgentNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: SdAgent,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (SdAgentNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(SdAgentNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
SdAgentNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class SdAgentNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSdAgentNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "sdAgent",
|
|
||||||
view: SdAgentNodeHtmlNode,
|
|
||||||
model: SdAgentNodeHtmlModel
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
||||||
</template>
|
</template>
|
||||||
<template #output>
|
<template #output>
|
||||||
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer v-if="!['img', 'video'].includes(props.properties.outputType)" :value="outputJsonData" copyable boxed sort theme="light" />
|
||||||
<el-image
|
<el-image
|
||||||
v-if="props.properties.outputType === 'img'"
|
v-if="props.properties.outputType === 'img'"
|
||||||
v-for="item in outputJsonData.imageUrl"
|
v-for="item in outputJsonData.imageUrl"
|
||||||
|
|||||||
@ -1,213 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
// @ts-ignore
|
|
||||||
import Sleep from "./sleep.vue";
|
|
||||||
import OuterNode from "../../components/OuterNode.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class SleepNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(OuterNode, {
|
|
||||||
model: props.model,
|
|
||||||
component: Sleep,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (SleepNodeHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(SleepNodeHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
SleepNodeHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class SleepNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 + 4,
|
|
||||||
y: y,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1"),
|
|
||||||
properties: { connectionType: "source" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSleepNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "sleep",
|
|
||||||
view: SleepNodeHtmlNode,
|
|
||||||
model: SleepNodeHtmlModel
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
||||||
</template>
|
</template>
|
||||||
<template #output>
|
<template #output>
|
||||||
<JsonViewer v-if="props.properties.outputType === 'json'" :value="outputJsonData" copyable boxed sort theme="light" />
|
<JsonViewer v-if="!['img', 'video'].includes(props.properties.outputType)" :value="outputJsonData" copyable boxed sort theme="light" />
|
||||||
<el-image
|
<el-image
|
||||||
v-if="props.properties.outputType === 'img'"
|
v-if="props.properties.outputType === 'img'"
|
||||||
v-for="item in outputJsonData.imageUrl"
|
v-for="item in outputJsonData.imageUrl"
|
||||||
|
|||||||
@ -1,191 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import StopLoop from "./stopLoop.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class StopLoopHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(StopLoop, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
text: props.model.text.value,
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.use(JsonViewer)
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class StopLoopHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y - 25,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerStopLoopNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "stopLoop",
|
|
||||||
view: StopLoopHtmlNode,
|
|
||||||
model: StopLoopHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import SubEndNode from "./subEnd.vue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class SubEndNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(SubEndNode, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class SubEndNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y - 25,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_3"),
|
|
||||||
properties: { connectionType: "target" },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSubEndNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "subEnd",
|
|
||||||
view: SubEndNodeHtmlNode,
|
|
||||||
model: SubEndNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import SubStartNode from "./subStart.vue";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class SubStartNodeHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(SubStartNode, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus); // 关键:单独注册ElementPlus
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class SubStartNodeHtmlModel extends HtmlNodeModel {
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
x: x + width / 2 - 24,
|
|
||||||
y: y - 25,
|
|
||||||
name: "right",
|
|
||||||
id: "".concat(this.id, "_1")
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSubStartNode(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "subStart",
|
|
||||||
view: SubStartNodeHtmlNode,
|
|
||||||
model: SubStartNodeHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,318 +0,0 @@
|
|||||||
// 导入 HtmlNode 及其模型,为后续继承做准备
|
|
||||||
import { HtmlNode, HtmlNodeModel, h as svgH } from "@logicflow/core";
|
|
||||||
// 导入 Vue 相关方法,用于渲染组件
|
|
||||||
import { createApp, h, nextTick } from "vue";
|
|
||||||
import ElementPlus from "element-plus";
|
|
||||||
// 导入 Vue 组件
|
|
||||||
import Switch from "./switch.vue";
|
|
||||||
import JsonViewer from 'vue3-json-viewer'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个 元素的 HTML 节点类,继承自 HtmlNode
|
|
||||||
* 该类负责在 HTML 中渲染 元素,并处理其交互逻辑
|
|
||||||
*/
|
|
||||||
class SwitchHtmlNode extends HtmlNode {
|
|
||||||
resizeObserver = null;
|
|
||||||
isMounted; // 标记组件是否已挂载
|
|
||||||
r; // 渲染函数
|
|
||||||
app; // Vue 应用实例
|
|
||||||
container = null;
|
|
||||||
|
|
||||||
static reusePool = new Map(); // 节点复用池
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param props 传递给节点的属性,包括模型、图模型等
|
|
||||||
*/
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.initVueApp(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
initVueApp(props) {
|
|
||||||
this.isMounted = false;
|
|
||||||
|
|
||||||
this.hideAnchor = false;
|
|
||||||
this.autoExpand = true; // 防止锚点被折叠
|
|
||||||
this.anchorsPreset = "default"; // 重置锚点预设
|
|
||||||
// 创建 元素的渲染函数
|
|
||||||
this.r = h(Switch, {
|
|
||||||
model: props.model,
|
|
||||||
graphModel: props.graphModel,
|
|
||||||
properties: {
|
|
||||||
...props.model.getProperties(),
|
|
||||||
},
|
|
||||||
text: props.model.text.value,
|
|
||||||
onContentChange: this.handleContentChange.bind(this),
|
|
||||||
onBindRef: this.handleComponentInstance.bind(this),
|
|
||||||
// 添加锚点相关回调
|
|
||||||
onAddAnchor: this.handleAddAnchor.bind(this),
|
|
||||||
onRemoveAnchor: this.handleRemoveAnchor.bind(this),
|
|
||||||
onChangeAnchor: this.handleChangeAnchor.bind(this)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建 Vue 应用实例,并指定渲染函数
|
|
||||||
this.app = createApp({
|
|
||||||
render: () => this.r,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 HTML 内容设置到指定的根元素上
|
|
||||||
* @param rootEl 根元素
|
|
||||||
*/
|
|
||||||
async setHtml(rootEl) {
|
|
||||||
const nodeId = this.props.model.id;
|
|
||||||
if (SwitchHtmlNode.reusePool.has(nodeId)) {
|
|
||||||
rootEl.appendChild(SwitchHtmlNode.reusePool.get(nodeId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isMounted) {
|
|
||||||
this.isMounted = true;
|
|
||||||
this.container = document.createElement("div");
|
|
||||||
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
|
|
||||||
|
|
||||||
rootEl.appendChild(this.container);
|
|
||||||
this.app.use(ElementPlus);
|
|
||||||
this.app.use(JsonViewer);
|
|
||||||
|
|
||||||
this.app.mount(this.container);
|
|
||||||
await nextTick();
|
|
||||||
this.setupSizeObserver();
|
|
||||||
SwitchHtmlNode.reusePool.set(nodeId, this.container);
|
|
||||||
} else {
|
|
||||||
this.r.component.props.properties = this.props.model.getProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点文本内容
|
|
||||||
* 对于元素,返回 null,因为其内容由特定组件渲染
|
|
||||||
* @returns {null}
|
|
||||||
*/
|
|
||||||
getText() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleComponentInstance(data) {
|
|
||||||
// 将组件实例保存到节点模型
|
|
||||||
this.props.model.setComponentInstance(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContentChange() {
|
|
||||||
// 内容变化时强制更新尺寸
|
|
||||||
this.updateNodeSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理添加锚点
|
|
||||||
handleAddAnchor(height, anchorId) {
|
|
||||||
const { model } = this.props
|
|
||||||
model.addAnchor(height, anchorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理删除锚点
|
|
||||||
handleRemoveAnchor(anchorId) {
|
|
||||||
this.props.model.removeAnchor(anchorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeAnchor(height, anchorId) {
|
|
||||||
const { model } = this.props
|
|
||||||
model.changeAnchorHeight(height, anchorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染完成后获取实际尺寸
|
|
||||||
updateNodeSize() {
|
|
||||||
if (this.container) {
|
|
||||||
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
|
|
||||||
const node = this.container.querySelector(".node__container");
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSizeObserver() {
|
|
||||||
// 首次渲染立即检测
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
// 持续监听变化
|
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
|
||||||
this.updateNodeSize();
|
|
||||||
});
|
|
||||||
this.resizeObserver.observe(this.container);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件卸载时移除监听
|
|
||||||
onDestroy() {
|
|
||||||
this.resizeObserver?.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel
|
|
||||||
* 该类主要设置节点的属性和样式
|
|
||||||
*/
|
|
||||||
class SwitchHtmlModel extends HtmlNodeModel {
|
|
||||||
|
|
||||||
initNodeData(data) {
|
|
||||||
super.initNodeData(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回动态锚点
|
|
||||||
addAnchor(height, anchorId) {
|
|
||||||
const properties = this.getProperties()
|
|
||||||
const anchor = properties?.anchor || []
|
|
||||||
anchor.push({
|
|
||||||
id: anchorId,
|
|
||||||
name: 'right',
|
|
||||||
x: this.x,
|
|
||||||
y: this.y,
|
|
||||||
height
|
|
||||||
})
|
|
||||||
this.setProperties({
|
|
||||||
...properties,
|
|
||||||
anchor
|
|
||||||
})
|
|
||||||
this.getDefaultAnchor()
|
|
||||||
this.initNodeData(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAnchor(anchorId) {
|
|
||||||
const properties = this.getProperties()
|
|
||||||
const anchor = properties?.anchor || []
|
|
||||||
let nweAnchor = anchor.filter(item => item.id !== anchorId);
|
|
||||||
this.setProperties({
|
|
||||||
...properties,
|
|
||||||
anchor: nweAnchor
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
changeAnchorHeight(height, anchorId) {
|
|
||||||
const properties = this.getProperties()
|
|
||||||
const anchor = properties?.anchor || []
|
|
||||||
anchor.forEach(item => {
|
|
||||||
if (item.id === anchorId) {
|
|
||||||
item.height = height
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.setProperties({
|
|
||||||
...properties,
|
|
||||||
anchor
|
|
||||||
})
|
|
||||||
this.getDefaultAnchor()
|
|
||||||
this.initNodeData(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件实例引用
|
|
||||||
setComponentInstance(instance) {
|
|
||||||
this.componentInstance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopover() {
|
|
||||||
this.componentInstance.closePopover()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证表单
|
|
||||||
async validateForm() {
|
|
||||||
if (this.componentInstance && this.componentInstance.validate) {
|
|
||||||
try {
|
|
||||||
const result = await this.componentInstance.validate();
|
|
||||||
if (result) {
|
|
||||||
return { valid: true, message: "验证通过" };
|
|
||||||
} else {
|
|
||||||
return { valid: false, message: "验证失败" };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return { valid: false, message: "验证失败" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { valid: true, message: "无验证方法" };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置节点属性
|
|
||||||
* 包括宽度、高度、文本编辑属性等
|
|
||||||
*/
|
|
||||||
setAttributes() {
|
|
||||||
// 初始设置为0,后续动态更新
|
|
||||||
this.width = 0;
|
|
||||||
this.height = 0;
|
|
||||||
this.text.editable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSize(width, height) {
|
|
||||||
this.width = width + 24;
|
|
||||||
this.height = height + 50;
|
|
||||||
// const properties = lf.getProperties(this.id)
|
|
||||||
// this.setProperties({
|
|
||||||
// ...properties,
|
|
||||||
// width: this.width,
|
|
||||||
// height: this.height
|
|
||||||
// })
|
|
||||||
this.initNodeData(this); // 触发节点重绘
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
|
|
||||||
getDefaultAnchor() {
|
|
||||||
let _a = this,
|
|
||||||
x = _a.x,
|
|
||||||
y = _a.y,
|
|
||||||
width = _a.width,
|
|
||||||
height = _a.height;
|
|
||||||
const properties = this.getProperties()
|
|
||||||
const anchor = properties?.anchor || []
|
|
||||||
// const { SCALE_Y } = this.graphModel.transformModel;
|
|
||||||
const ids = []
|
|
||||||
anchor.forEach(item => {
|
|
||||||
ids.push(item.id)
|
|
||||||
if (item.id !== "".concat(this.id, "_else")) {
|
|
||||||
item.x = x + width / 2 - 40
|
|
||||||
// item.y = (y - height / 2 + item.height + 60) / SCALE_Y
|
|
||||||
item.y = y - height / 2 + item.height + 60
|
|
||||||
} else {
|
|
||||||
item.x = x + width / 2 - 40
|
|
||||||
item.y = y + height / 2 - 80
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return [
|
|
||||||
// {
|
|
||||||
// x: x + width / 2 - 40,
|
|
||||||
// y: y + height / 2 - 80,
|
|
||||||
// name: "right",
|
|
||||||
// id: "".concat(this.id, "_else")
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
x: x - width / 2,
|
|
||||||
y: y - 25,
|
|
||||||
name: "left",
|
|
||||||
id: "".concat(this.id, "_entry")
|
|
||||||
},
|
|
||||||
...anchor
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点轮廓样式
|
|
||||||
* 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果
|
|
||||||
* @returns {object} 节点轮廓样式
|
|
||||||
*/
|
|
||||||
getOutlineStyle() {
|
|
||||||
const style = super.getOutlineStyle();
|
|
||||||
style.stroke = "none";
|
|
||||||
// style.hover.stroke = "none";
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出方法注册
|
|
||||||
export function registerSwitch(lf) {
|
|
||||||
lf.register({
|
|
||||||
type: "branch",
|
|
||||||
view: SwitchHtmlNode,
|
|
||||||
model: SwitchHtmlModel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -455,14 +455,14 @@ const closePopover = () => {
|
|||||||
|
|
||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
const handleInputFocus = (e) => {
|
const handleInputFocus = (e) => {
|
||||||
// 阻止事件冒泡到 LogicFlow
|
// 阻止事件冒泡到流程画布
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleInputKeydown = (e) => {
|
const handleInputKeydown = (e) => {
|
||||||
// 阻止事件冒泡到 Logic Flow 节点,避免被其事件拦截
|
// 阻止事件冒泡到流程节点,避免被画布快捷键拦截
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 可选:明确放行 Ctrl+V(增强兼容性)
|
// 可选:明确放行 Ctrl+V(增强兼容性)
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
||||||
|
|||||||
86
src/views/flow/vue-flow/FlowEdge.vue
Normal file
86
src/views/flow/vue-flow/FlowEdge.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<BaseEdge
|
||||||
|
:id="id"
|
||||||
|
:path="edgePath"
|
||||||
|
:marker-end="markerEnd"
|
||||||
|
:interaction-width="interactionWidth || 32"
|
||||||
|
:style="style"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeLabelRenderer v-if="selected && !flowStore.disableForm">
|
||||||
|
<div class="edge-delete-action nodrag nopan" :style="deleteActionStyle">
|
||||||
|
<el-tooltip content="删除连线" placement="top">
|
||||||
|
<button type="button" aria-label="删除连线" @click.stop="deleteEdge">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@vue-flow/core";
|
||||||
|
import { Delete } from "@element-plus/icons-vue";
|
||||||
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
sourceX: Number,
|
||||||
|
sourceY: Number,
|
||||||
|
targetX: Number,
|
||||||
|
targetY: Number,
|
||||||
|
sourcePosition: String,
|
||||||
|
targetPosition: String,
|
||||||
|
markerEnd: String,
|
||||||
|
interactionWidth: Number,
|
||||||
|
selected: Boolean,
|
||||||
|
style: Object,
|
||||||
|
});
|
||||||
|
|
||||||
|
const flowStore = useFlowStore();
|
||||||
|
const pathData = computed(() => getBezierPath({
|
||||||
|
sourceX: props.sourceX,
|
||||||
|
sourceY: props.sourceY,
|
||||||
|
sourcePosition: props.sourcePosition,
|
||||||
|
targetX: props.targetX,
|
||||||
|
targetY: props.targetY,
|
||||||
|
targetPosition: props.targetPosition,
|
||||||
|
}));
|
||||||
|
const edgePath = computed(() => pathData.value[0]);
|
||||||
|
const deleteActionStyle = computed(() => ({
|
||||||
|
transform: `translate(-50%, -50%) translate(${pathData.value[1]}px, ${pathData.value[2] - 24}px)`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const deleteEdge = () => {
|
||||||
|
window.lf?.deleteEdge(props.id);
|
||||||
|
window.lf?.clearSelectElements();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.edge-delete-action {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: grid;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #f2b8b5;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #d93025;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(20, 33, 61, 0.18);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #d93025;
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/views/flow/vue-flow/FlowNode.vue
Normal file
202
src/views/flow/vue-flow/FlowNode.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flow-node-shell"
|
||||||
|
:class="[`flow-node-shell--${flowType}`, { 'flow-node-shell--selected': selected }]"
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
v-if="showTarget"
|
||||||
|
:id="targetHandleId"
|
||||||
|
type="target"
|
||||||
|
:position="Position.Left"
|
||||||
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
class="flow-handle flow-handle--target"
|
||||||
|
/>
|
||||||
|
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup && flowType !== 'branch' }">
|
||||||
|
<component
|
||||||
|
:is="nodeComponent"
|
||||||
|
ref="componentRef"
|
||||||
|
:model="model"
|
||||||
|
:graph-model="model"
|
||||||
|
:properties="properties"
|
||||||
|
@content-change="contentChanged"
|
||||||
|
@bind-ref="bindComponent"
|
||||||
|
@add-anchor="addAnchor"
|
||||||
|
@remove-anchor="removeAnchor"
|
||||||
|
@change-anchor="changeAnchor"
|
||||||
|
@add-to-group="addToGroup"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="flowType === 'branch'">
|
||||||
|
<template v-for="anchor in branchAnchors" :key="anchor.id">
|
||||||
|
<Handle
|
||||||
|
:id="anchor.id"
|
||||||
|
type="source"
|
||||||
|
:position="Position.Right"
|
||||||
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
class="flow-handle flow-handle--source flow-handle--branch"
|
||||||
|
:style="{ top: `${Math.max(76, Number(anchor.height || 0) + 60)}px` }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<Handle
|
||||||
|
v-else-if="showSource"
|
||||||
|
:id="`${id}_1`"
|
||||||
|
type="source"
|
||||||
|
:position="Position.Right"
|
||||||
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
class="flow-handle flow-handle--source"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
||||||
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
|
import StartNode from "../nodes/common/startNode.vue";
|
||||||
|
import EndNode from "../nodes/common/endNode.vue";
|
||||||
|
import ServiceNode from "../nodes/common/serviceNode.vue";
|
||||||
|
import SelectAreaNode from "../nodes/common/selectAreaNode.vue";
|
||||||
|
import LoopNode from "../nodes/function/loop.vue";
|
||||||
|
import BranchNode from "../nodes/function/switch.vue";
|
||||||
|
import StopLoopNode from "../nodes/function/stopLoop.vue";
|
||||||
|
import SubStartNode from "../nodes/function/subStart.vue";
|
||||||
|
import SubEndNode from "../nodes/function/subEnd.vue";
|
||||||
|
import SleepNode from "../nodes/function/sleep.vue";
|
||||||
|
import HttpNode from "../nodes/function/httpNode.vue";
|
||||||
|
import CodeNode from "../nodes/function/codeNode.vue";
|
||||||
|
import CurrentLoopNode from "../nodes/function/currentLoop.vue";
|
||||||
|
import SdAgentNode from "../nodes/function/sdAgent.vue";
|
||||||
|
import RecognizeNode from "../nodes/function/recognize.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: String,
|
||||||
|
data: Object,
|
||||||
|
selected: Boolean,
|
||||||
|
connectable: [Boolean, Number, String, Function],
|
||||||
|
});
|
||||||
|
|
||||||
|
const componentMap = {
|
||||||
|
start: StartNode,
|
||||||
|
end: EndNode,
|
||||||
|
serviceNode: ServiceNode,
|
||||||
|
selectArea: SelectAreaNode,
|
||||||
|
loop: LoopNode,
|
||||||
|
branch: BranchNode,
|
||||||
|
stopLoop: StopLoopNode,
|
||||||
|
subStart: SubStartNode,
|
||||||
|
subEnd: SubEndNode,
|
||||||
|
sleep: SleepNode,
|
||||||
|
http: HttpNode,
|
||||||
|
code: CodeNode,
|
||||||
|
currentLoop: CurrentLoopNode,
|
||||||
|
sdAgent: SdAgentNode,
|
||||||
|
recognize: RecognizeNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const flowStore = useFlowStore();
|
||||||
|
const vueFlow = useVueFlow("main-flow");
|
||||||
|
const componentRef = ref();
|
||||||
|
const flowType = computed(() => props.data.flowType);
|
||||||
|
const properties = computed(() => props.data.properties || {});
|
||||||
|
const nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
|
||||||
|
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
|
||||||
|
const showTarget = computed(() => !["start", "subStart", "selectArea"].includes(flowType.value));
|
||||||
|
const showSource = computed(() => !["end", "stopLoop", "subEnd", "selectArea", "branch"].includes(flowType.value));
|
||||||
|
const targetHandleId = computed(() => flowType.value === "branch" ? `${props.id}_entry` : `${props.id}_3`);
|
||||||
|
const branchAnchors = computed(() => {
|
||||||
|
const reservedIds = new Set([`${props.id}_1`, `${props.id}_3`, `${props.id}_entry`]);
|
||||||
|
const conditionDefs = properties.value.nodeParams || properties.value.conditions || [];
|
||||||
|
const conditionIds = new Set(conditionDefs.map((condition) => condition?.id).filter(Boolean));
|
||||||
|
const elseId = `${props.id}_else`;
|
||||||
|
const seen = new Set();
|
||||||
|
return (properties.value.anchor || []).filter((anchor) => {
|
||||||
|
if (!anchor?.id || reservedIds.has(anchor.id) || seen.has(anchor.id)) return false;
|
||||||
|
if (conditionIds.size && anchor.id !== elseId && !conditionIds.has(anchor.id)) return false;
|
||||||
|
seen.add(anchor.id);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const model = computed(() => window.lf?.getNodeModelById(props.id));
|
||||||
|
|
||||||
|
const syncAnchors = (anchors) => {
|
||||||
|
window.lf?.setProperties(props.id, { ...window.lf.getProperties(props.id), anchor: anchors });
|
||||||
|
vueFlow.updateNodeInternals([props.id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAnchor = (height, anchorId) => {
|
||||||
|
const anchors = [...branchAnchors.value];
|
||||||
|
if (!anchors.some((anchor) => anchor.id === anchorId)) {
|
||||||
|
anchors.push({ id: anchorId, name: "right", height });
|
||||||
|
syncAnchors(anchors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAnchor = (anchorId) => syncAnchors(branchAnchors.value.filter((anchor) => anchor.id !== anchorId));
|
||||||
|
const changeAnchor = (height, anchorId) => syncAnchors(branchAnchors.value.map((anchor) => anchor.id === anchorId ? { ...anchor, height } : anchor));
|
||||||
|
const contentChanged = () => {
|
||||||
|
vueFlow.updateNodeInternals([props.id]);
|
||||||
|
window.lf?.markChanged();
|
||||||
|
};
|
||||||
|
const bindComponent = (instance) => window.lf?.registerComponentInstance(props.id, instance);
|
||||||
|
const addToGroup = (childId, dropCenter) => window.lf?.addToGroup(props.id, childId, dropCenter);
|
||||||
|
|
||||||
|
onMounted(() => bindComponent(componentRef.value));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.flow-node-shell {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&--loop,
|
||||||
|
&--selectArea {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loop.flow-node-shell--selected .flow-node-content {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected .flow-node-content {
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid #2f80ed;
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-node-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-node-card {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e8ef;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.08);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-handle {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
background: #ff6b35;
|
||||||
|
box-shadow: 0 0 0 1px #ff6b35;
|
||||||
|
z-index: 8;
|
||||||
|
|
||||||
|
&--target { left: -7px; }
|
||||||
|
&--source { right: -7px; }
|
||||||
|
&--branch { transform: translate(50%, -50%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
578
src/views/flow/vue-flow/adapter.js
Normal file
578
src/views/flow/vue-flow/adapter.js
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
import { nextTick } from "vue";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { MarkerType } from "@vue-flow/core";
|
||||||
|
|
||||||
|
const clone = (value) => JSON.parse(JSON.stringify(value ?? {}));
|
||||||
|
|
||||||
|
const GROUP_TYPES = new Set(["loop", "selectArea"]);
|
||||||
|
const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]);
|
||||||
|
const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]);
|
||||||
|
const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 };
|
||||||
|
const LOOP_MIN_SIZE = { width: 520, height: 360 };
|
||||||
|
|
||||||
|
const loopChildExtent = () => ({
|
||||||
|
range: "parent",
|
||||||
|
padding: [
|
||||||
|
LOOP_BODY_INSET.top,
|
||||||
|
LOOP_BODY_INSET.right,
|
||||||
|
LOOP_BODY_INSET.bottom,
|
||||||
|
LOOP_BODY_INSET.left,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeSize = (node) => {
|
||||||
|
const width = Number(node.properties?.width || node.width);
|
||||||
|
const height = Number(node.properties?.height || node.height);
|
||||||
|
if (Number.isFinite(width) && Number.isFinite(height)) {
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
if (node.type === "branch") return { width: 748, height: 420 };
|
||||||
|
if (GROUP_TYPES.has(node.type)) return { width: 720, height: 460 };
|
||||||
|
if (TARGET_ONLY_TYPES.has(node.type) || SOURCE_ONLY_TYPES.has(node.type)) {
|
||||||
|
return { width: 328, height: 120 };
|
||||||
|
}
|
||||||
|
return { width: 372, height: 180 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const edgeId = () => `edge_${uuid().replace(/-/g, "")}`;
|
||||||
|
const nodeId = () => uuid().replace(/-/g, "");
|
||||||
|
|
||||||
|
const toVueNode = (node) => {
|
||||||
|
const normalized = clone(node);
|
||||||
|
normalized.id ||= nodeId();
|
||||||
|
normalized.properties ||= {};
|
||||||
|
const size = nodeSize(normalized);
|
||||||
|
const center = {
|
||||||
|
x: Number(normalized.x) || 0,
|
||||||
|
y: Number(normalized.y) || 0,
|
||||||
|
};
|
||||||
|
const position = {
|
||||||
|
x: center.x - size.width / 2,
|
||||||
|
y: center.y - size.height / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dimensions = GROUP_TYPES.has(normalized.type)
|
||||||
|
? { width: size.width, height: size.height }
|
||||||
|
: { width: size.width };
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: normalized.id,
|
||||||
|
type: "flow-node",
|
||||||
|
position,
|
||||||
|
...dimensions,
|
||||||
|
draggable: normalized.draggable !== false,
|
||||||
|
selectable: true,
|
||||||
|
deletable: !SOURCE_ONLY_TYPES.has(normalized.type) && normalized.type !== "end",
|
||||||
|
connectable: true,
|
||||||
|
zIndex: GROUP_TYPES.has(normalized.type) ? 0 : 2,
|
||||||
|
data: {
|
||||||
|
flowType: normalized.type,
|
||||||
|
properties: normalized.properties,
|
||||||
|
children: Array.isArray(normalized.children) ? normalized.children : [],
|
||||||
|
text: normalized.text,
|
||||||
|
backend: normalized,
|
||||||
|
originalCenter: center,
|
||||||
|
originalPosition: position,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toVueEdge = (edge) => {
|
||||||
|
const normalized = clone(edge);
|
||||||
|
normalized.id ||= edgeId();
|
||||||
|
return {
|
||||||
|
id: normalized.id,
|
||||||
|
source: normalized.sourceNodeId,
|
||||||
|
target: normalized.targetNodeId,
|
||||||
|
sourceHandle: normalized.sourceAnchorId || null,
|
||||||
|
targetHandle: normalized.targetAnchorId || null,
|
||||||
|
type: "flow-edge",
|
||||||
|
animated: true,
|
||||||
|
selectable: true,
|
||||||
|
focusable: true,
|
||||||
|
deletable: true,
|
||||||
|
interactionWidth: 32,
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: "#ff6b35",
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
},
|
||||||
|
style: { stroke: "#ff6b35", strokeWidth: 2 },
|
||||||
|
data: { backend: normalized },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachChildrenToGroups = (nodes) => {
|
||||||
|
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
|
||||||
|
nodes.forEach((child) => {
|
||||||
|
const parentId = child.data.properties?.parentId;
|
||||||
|
const parent = nodeLookup.get(parentId);
|
||||||
|
if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return;
|
||||||
|
|
||||||
|
const isLoop = parent.data.flowType === "loop";
|
||||||
|
const relativePosition = {
|
||||||
|
x: isLoop
|
||||||
|
? Math.max(child.position.x - parent.position.x, LOOP_BODY_INSET.left)
|
||||||
|
: child.position.x - parent.position.x,
|
||||||
|
y: isLoop
|
||||||
|
? Math.max(child.position.y - parent.position.y, LOOP_BODY_INSET.top)
|
||||||
|
: child.position.y - parent.position.y,
|
||||||
|
};
|
||||||
|
child.parentNode = parentId;
|
||||||
|
child.extent = isLoop ? loopChildExtent() : "parent";
|
||||||
|
child.expandParent = false;
|
||||||
|
child.position = relativePosition;
|
||||||
|
child.zIndex = 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.filter((node) => node.data.flowType === "loop").forEach((group) => {
|
||||||
|
const children = nodes.filter((node) => node.parentNode === group.id);
|
||||||
|
if (!children.length) return;
|
||||||
|
const requiredWidth = Math.max(...children.map((child) => (
|
||||||
|
child.position.x + Number(child.data.size?.width || 0) + LOOP_BODY_INSET.right
|
||||||
|
)));
|
||||||
|
const requiredHeight = Math.max(...children.map((child) => (
|
||||||
|
child.position.y + Number(child.data.size?.height || 0) + LOOP_BODY_INSET.bottom
|
||||||
|
)));
|
||||||
|
const width = Math.max(Number(group.data.size?.width || 0), LOOP_MIN_SIZE.width, requiredWidth);
|
||||||
|
const height = Math.max(Number(group.data.size?.height || 0), LOOP_MIN_SIZE.height, requiredHeight);
|
||||||
|
group.width = width;
|
||||||
|
group.height = height;
|
||||||
|
group.data.size = { width, height };
|
||||||
|
group.data.properties = { ...group.data.properties, width, height };
|
||||||
|
});
|
||||||
|
|
||||||
|
return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toVueFlowData = (graph = {}) => ({
|
||||||
|
nodes: attachChildrenToGroups((graph.nodes || []).map(toVueNode)),
|
||||||
|
edges: (graph.edges || []).map(toVueEdge),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createFlowFacade = (store) => {
|
||||||
|
const handlers = new Map();
|
||||||
|
const componentInstances = new Map();
|
||||||
|
const loopLayoutStates = new Map();
|
||||||
|
let historySuspended = false;
|
||||||
|
let history = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
|
||||||
|
const getNodes = () => store.getNodes.value || [];
|
||||||
|
const getEdges = () => store.getEdges.value || [];
|
||||||
|
|
||||||
|
const backendNode = (node) => {
|
||||||
|
const result = clone(node.data.backend);
|
||||||
|
const size = {
|
||||||
|
width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0),
|
||||||
|
height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
|
||||||
|
};
|
||||||
|
const parent = node.parentNode ? store.findNode(node.parentNode) : null;
|
||||||
|
const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 };
|
||||||
|
const absolutePosition = parent
|
||||||
|
? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y }
|
||||||
|
: (node.position || node.computedPosition);
|
||||||
|
result.id = node.id;
|
||||||
|
result.type = node.data.flowType;
|
||||||
|
result.x = absolutePosition.x + size.width / 2;
|
||||||
|
result.y = absolutePosition.y + size.height / 2;
|
||||||
|
result.properties = clone(node.data.properties || {});
|
||||||
|
if (GROUP_TYPES.has(result.type)) {
|
||||||
|
result.properties.width = size.width;
|
||||||
|
result.properties.height = size.height;
|
||||||
|
}
|
||||||
|
if (node.data.children?.length || GROUP_TYPES.has(result.type)) {
|
||||||
|
result.children = [...(node.data.children || [])];
|
||||||
|
} else {
|
||||||
|
delete result.children;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const backendEdge = (edge) => ({
|
||||||
|
...clone(edge.data?.backend),
|
||||||
|
id: edge.id,
|
||||||
|
type: edge.data?.backend?.type || "bezier",
|
||||||
|
sourceNodeId: edge.source,
|
||||||
|
targetNodeId: edge.target,
|
||||||
|
sourceAnchorId: edge.sourceHandle || `${edge.source}_1`,
|
||||||
|
targetAnchorId: edge.targetHandle || `${edge.target}_3`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = (name, payload) => {
|
||||||
|
(handlers.get(name) || []).forEach((handler) => handler(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
const recordHistory = () => {
|
||||||
|
if (historySuspended) return;
|
||||||
|
const graph = facade.getGraphData();
|
||||||
|
const serialized = JSON.stringify(graph);
|
||||||
|
if (history[historyIndex]?.serialized === serialized) return;
|
||||||
|
history = history.slice(0, historyIndex + 1);
|
||||||
|
history.push({ serialized, graph: clone(graph) });
|
||||||
|
if (history.length > 100) history.shift();
|
||||||
|
historyIndex = history.length - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const changed = () => {
|
||||||
|
if (historySuspended) return;
|
||||||
|
recordHistory();
|
||||||
|
emit("history:change", { data: facade.getGraphData() });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeRect = (node) => {
|
||||||
|
const parent = node.parentNode ? store.findNode(node.parentNode) : null;
|
||||||
|
const parentPosition = parent?.position || parent?.computedPosition || { x: 0, y: 0 };
|
||||||
|
const position = parent
|
||||||
|
? { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y }
|
||||||
|
: (node.position || node.computedPosition);
|
||||||
|
return {
|
||||||
|
x: Number(position?.x || 0),
|
||||||
|
y: Number(position?.y || 0),
|
||||||
|
width: Number(node.width || node.dimensions?.width || node.data.size?.width || 0),
|
||||||
|
height: Number(node.height || node.dimensions?.height || node.data.size?.height || 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLoopLayoutState = (group) => {
|
||||||
|
const current = getNodeRect(group);
|
||||||
|
const existing = loopLayoutStates.get(group.id);
|
||||||
|
if (!existing) {
|
||||||
|
const state = { base: { ...current }, last: { ...current } };
|
||||||
|
loopLayoutStates.set(group.id, state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeChanged = Math.abs(current.width - existing.last.width) > 1
|
||||||
|
|| Math.abs(current.height - existing.last.height) > 1;
|
||||||
|
if (sizeChanged) {
|
||||||
|
existing.base = { ...current };
|
||||||
|
} else {
|
||||||
|
const dx = current.x - existing.last.x;
|
||||||
|
const dy = current.y - existing.last.y;
|
||||||
|
existing.base.x += dx;
|
||||||
|
existing.base.y += dy;
|
||||||
|
}
|
||||||
|
existing.last = { ...current };
|
||||||
|
return existing;
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutLoop = (group, activeChildId, activePosition) => {
|
||||||
|
const state = getLoopLayoutState(group);
|
||||||
|
const children = getNodes().filter((node) => node.parentNode === group.id);
|
||||||
|
if (!children.length) return false;
|
||||||
|
|
||||||
|
const snapshots = children.map((child) => {
|
||||||
|
const rect = getNodeRect(child);
|
||||||
|
const position = child.id === activeChildId && activePosition
|
||||||
|
? { x: activePosition.x, y: activePosition.y }
|
||||||
|
: { x: rect.x, y: rect.y };
|
||||||
|
return { child, position, width: rect.width, height: rect.height };
|
||||||
|
});
|
||||||
|
const contentLeft = Math.min(...snapshots.map(({ position }) => position.x));
|
||||||
|
const contentTop = Math.min(...snapshots.map(({ position }) => position.y));
|
||||||
|
const contentRight = Math.max(...snapshots.map(({ position, width }) => position.x + width));
|
||||||
|
const contentBottom = Math.max(...snapshots.map(({ position, height }) => position.y + height));
|
||||||
|
const left = Math.min(state.base.x, contentLeft - LOOP_BODY_INSET.left);
|
||||||
|
const top = Math.min(state.base.y, contentTop - LOOP_BODY_INSET.top);
|
||||||
|
const right = Math.max(
|
||||||
|
state.base.x + Math.max(state.base.width, LOOP_MIN_SIZE.width),
|
||||||
|
contentRight + LOOP_BODY_INSET.right,
|
||||||
|
);
|
||||||
|
const bottom = Math.max(
|
||||||
|
state.base.y + Math.max(state.base.height, LOOP_MIN_SIZE.height),
|
||||||
|
contentBottom + LOOP_BODY_INSET.bottom,
|
||||||
|
);
|
||||||
|
const width = right - left;
|
||||||
|
const height = bottom - top;
|
||||||
|
const properties = { ...group.data.properties, width, height };
|
||||||
|
|
||||||
|
store.updateNode(group.id, {
|
||||||
|
position: { x: left, y: top },
|
||||||
|
computedPosition: { ...group.computedPosition, x: left, y: top },
|
||||||
|
dimensions: { width, height },
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
store.updateNodeData(group.id, {
|
||||||
|
...group.data,
|
||||||
|
properties,
|
||||||
|
size: { width, height },
|
||||||
|
}, { replace: true });
|
||||||
|
snapshots.forEach(({ child, position }) => {
|
||||||
|
store.updateNode(child.id, {
|
||||||
|
position: { x: position.x - left, y: position.y - top },
|
||||||
|
computedPosition: { ...child.computedPosition, x: position.x, y: position.y },
|
||||||
|
extent: loopChildExtent(),
|
||||||
|
expandParent: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
state.last = { x: left, y: top, width, height };
|
||||||
|
nextTick(() => store.updateNodeInternals([group.id, ...children.map((child) => child.id)]));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAnchors = (node) => {
|
||||||
|
const type = node.data.flowType;
|
||||||
|
if (type === "branch") {
|
||||||
|
return [
|
||||||
|
{ id: `${node.id}_entry`, name: "left" },
|
||||||
|
...(node.data.properties?.anchor || []).map((item) => ({ ...item, name: "right" })),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (TARGET_ONLY_TYPES.has(type)) return [{ id: `${node.id}_3`, name: "left" }];
|
||||||
|
if (SOURCE_ONLY_TYPES.has(type)) return [{ id: `${node.id}_1`, name: "right" }];
|
||||||
|
if (type === "selectArea") return [];
|
||||||
|
return [
|
||||||
|
{ id: `${node.id}_1`, name: "right" },
|
||||||
|
{ id: `${node.id}_3`, name: "left" },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const modelFor = (id) => {
|
||||||
|
const node = store.findNode(id);
|
||||||
|
if (!node) return null;
|
||||||
|
const model = {
|
||||||
|
id,
|
||||||
|
type: node.data.flowType,
|
||||||
|
get x() { return backendNode(node).x; },
|
||||||
|
get y() { return backendNode(node).y; },
|
||||||
|
get width() { return node.dimensions?.width || node.data.size?.width || 0; },
|
||||||
|
get height() { return node.dimensions?.height || node.data.size?.height || 0; },
|
||||||
|
get anchors() { return getAnchors(node); },
|
||||||
|
get children() { return new Set(node.data.children || []); },
|
||||||
|
set children(value) {
|
||||||
|
const children = Array.from(value || []);
|
||||||
|
store.updateNodeData(id, { ...node.data, children }, { replace: true });
|
||||||
|
changed();
|
||||||
|
},
|
||||||
|
get draggable() { return node.draggable !== false; },
|
||||||
|
set draggable(value) { store.updateNode(id, { draggable: Boolean(value) }); },
|
||||||
|
getProperties: () => facade.getProperties(id),
|
||||||
|
setProperties: (properties) => facade.setProperties(id, properties),
|
||||||
|
setComponentInstance: (instance) => componentInstances.set(id, instance),
|
||||||
|
closePopover: () => componentInstances.get(id)?.closePopover?.(),
|
||||||
|
updateSize: () => store.updateNodeInternals([id]),
|
||||||
|
setCustomProperties: () => store.updateNodeInternals([id]),
|
||||||
|
};
|
||||||
|
return model;
|
||||||
|
};
|
||||||
|
|
||||||
|
const facade = {
|
||||||
|
on(name, handler) {
|
||||||
|
const list = handlers.get(name) || [];
|
||||||
|
list.push(handler);
|
||||||
|
handlers.set(name, list);
|
||||||
|
return () => facade.off(name, handler);
|
||||||
|
},
|
||||||
|
once(name, handler) {
|
||||||
|
const wrapped = (payload) => {
|
||||||
|
facade.off(name, wrapped);
|
||||||
|
handler(payload);
|
||||||
|
};
|
||||||
|
facade.on(name, wrapped);
|
||||||
|
},
|
||||||
|
off(name, handler) {
|
||||||
|
if (!handler) handlers.delete(name);
|
||||||
|
else handlers.set(name, (handlers.get(name) || []).filter((item) => item !== handler));
|
||||||
|
},
|
||||||
|
emit,
|
||||||
|
markChanged: changed,
|
||||||
|
getGraphData() {
|
||||||
|
return {
|
||||||
|
nodes: getNodes().map(backendNode),
|
||||||
|
edges: getEdges().map(backendEdge),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render(graph, options = {}) {
|
||||||
|
historySuspended = true;
|
||||||
|
loopLayoutStates.clear();
|
||||||
|
const data = toVueFlowData(graph);
|
||||||
|
store.setNodes(data.nodes);
|
||||||
|
store.setEdges(data.edges);
|
||||||
|
nextTick(() => {
|
||||||
|
store.updateNodeInternals();
|
||||||
|
historySuspended = false;
|
||||||
|
if (options.resetHistory !== false) {
|
||||||
|
history = [];
|
||||||
|
historyIndex = -1;
|
||||||
|
recordHistory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addNode(node) {
|
||||||
|
const vueNode = toVueNode(node);
|
||||||
|
store.addNodes(vueNode);
|
||||||
|
changed();
|
||||||
|
return backendNode(vueNode);
|
||||||
|
},
|
||||||
|
deleteNode(id) {
|
||||||
|
const target = store.findNode(id);
|
||||||
|
const parentId = target?.parentNode;
|
||||||
|
const childIds = target?.data?.children || [];
|
||||||
|
const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0;
|
||||||
|
const nodeIds = removeChildren ? [id, ...childIds] : id;
|
||||||
|
const removedIds = new Set(Array.isArray(nodeIds) ? nodeIds : [nodeIds]);
|
||||||
|
store.removeNodes(nodeIds, true, removeChildren);
|
||||||
|
getNodes().forEach((node) => {
|
||||||
|
if (node.data.children?.includes(id) && !removedIds.has(node.id)) {
|
||||||
|
store.updateNodeData(node.id, {
|
||||||
|
...node.data,
|
||||||
|
children: node.data.children.filter((childId) => childId !== id),
|
||||||
|
}, { replace: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
componentInstances.delete(id);
|
||||||
|
childIds.forEach((childId) => componentInstances.delete(childId));
|
||||||
|
changed();
|
||||||
|
if (parentId) nextTick(() => facade.resizeGroupToChildren(parentId));
|
||||||
|
},
|
||||||
|
addEdge(edge) {
|
||||||
|
const vueEdge = toVueEdge(edge);
|
||||||
|
store.addEdges(vueEdge);
|
||||||
|
changed();
|
||||||
|
return backendEdge(vueEdge);
|
||||||
|
},
|
||||||
|
deleteEdge(id) {
|
||||||
|
store.removeEdges(id);
|
||||||
|
changed();
|
||||||
|
},
|
||||||
|
deleteEdgeByNodeId(id) {
|
||||||
|
store.removeEdges(getEdges().filter((edge) => edge.source === id || edge.target === id));
|
||||||
|
changed();
|
||||||
|
},
|
||||||
|
getProperties(id) {
|
||||||
|
return clone(store.findNode(id)?.data?.properties || {});
|
||||||
|
},
|
||||||
|
setProperties(id, properties) {
|
||||||
|
const node = store.findNode(id);
|
||||||
|
if (!node) return;
|
||||||
|
store.updateNodeData(id, { ...node.data, properties: clone(properties) }, { replace: true });
|
||||||
|
nextTick(() => store.updateNodeInternals([id]));
|
||||||
|
changed();
|
||||||
|
},
|
||||||
|
getNodeModelById: modelFor,
|
||||||
|
getSelectElements() {
|
||||||
|
return {
|
||||||
|
nodes: (store.getSelectedNodes.value || []).map(backendNode),
|
||||||
|
edges: (store.getSelectedEdges.value || []).map(backendEdge),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
clearSelectElements() {
|
||||||
|
store.removeSelectedElements();
|
||||||
|
},
|
||||||
|
selectElementById(id) {
|
||||||
|
store.removeSelectedElements();
|
||||||
|
if (!id) return;
|
||||||
|
const node = store.findNode(id);
|
||||||
|
if (node) store.addSelectedNodes([node]);
|
||||||
|
const edge = store.findEdge(id);
|
||||||
|
if (edge) store.addSelectedEdges([edge]);
|
||||||
|
},
|
||||||
|
focusOn({ id }) {
|
||||||
|
const node = store.findNode(id);
|
||||||
|
if (!node) return;
|
||||||
|
const center = backendNode(node);
|
||||||
|
store.setCenter(center.x, center.y, { zoom: Math.max(store.viewport.value?.zoom || 1, 0.7), duration: 300 });
|
||||||
|
},
|
||||||
|
fitView() {
|
||||||
|
return store.fitView({ padding: 0.18, duration: 300 });
|
||||||
|
},
|
||||||
|
translateCenter() {
|
||||||
|
return facade.fitView();
|
||||||
|
},
|
||||||
|
zoom(scale, center) {
|
||||||
|
if (Array.isArray(center) && center.length === 2) {
|
||||||
|
return store.setCenter(center[0], center[1], { zoom: scale, duration: 250 });
|
||||||
|
}
|
||||||
|
return store.zoomTo(scale, { duration: 250 });
|
||||||
|
},
|
||||||
|
getPointByClient(x, y) {
|
||||||
|
const point = store.screenToFlowCoordinate({ x, y });
|
||||||
|
return { canvasOverlayPosition: point, domOverlayPosition: point };
|
||||||
|
},
|
||||||
|
addToGroup(groupId, childId, dropCenter) {
|
||||||
|
const group = store.findNode(groupId);
|
||||||
|
const child = store.findNode(childId);
|
||||||
|
if (!group || !child || GROUP_TYPES.has(child.data.flowType)) return;
|
||||||
|
const graph = facade.getGraphData();
|
||||||
|
const groupData = graph.nodes.find((node) => node.id === groupId);
|
||||||
|
const childData = graph.nodes.find((node) => node.id === childId);
|
||||||
|
if (!groupData || !childData) return;
|
||||||
|
groupData.children = Array.from(new Set([...(groupData.children || []), childId]));
|
||||||
|
childData.properties = { ...(childData.properties || {}), parentId: groupId };
|
||||||
|
if (dropCenter) {
|
||||||
|
childData.x = dropCenter.x;
|
||||||
|
childData.y = dropCenter.y;
|
||||||
|
}
|
||||||
|
facade.render(graph, { resetHistory: false });
|
||||||
|
nextTick(() => {
|
||||||
|
store.updateNodeInternals([groupId, childId]);
|
||||||
|
changed();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
prepareLoopChildDrag(childId) {
|
||||||
|
const child = store.findNode(childId);
|
||||||
|
const group = child?.parentNode ? store.findNode(child.parentNode) : null;
|
||||||
|
if (!group || group.data.flowType !== "loop") return;
|
||||||
|
store.updateNode(childId, {
|
||||||
|
extent: loopChildExtent(),
|
||||||
|
expandParent: false,
|
||||||
|
});
|
||||||
|
getLoopLayoutState(group);
|
||||||
|
const rect = getNodeRect(child);
|
||||||
|
return { groupId: group.id, position: { x: rect.x, y: rect.y } };
|
||||||
|
},
|
||||||
|
layoutLoopDuringDrag(groupId, childId, position) {
|
||||||
|
const group = store.findNode(groupId);
|
||||||
|
if (!group || group.data.flowType !== "loop" || !position) return;
|
||||||
|
layoutLoop(group, childId, position);
|
||||||
|
},
|
||||||
|
resizeGroupToChildren(groupId, options = {}) {
|
||||||
|
const group = store.findNode(groupId);
|
||||||
|
if (!group || group.data.flowType !== "loop") return;
|
||||||
|
if (layoutLoop(group) && options.markChanged !== false) changed();
|
||||||
|
},
|
||||||
|
moveNodeBy(id, dx, dy) {
|
||||||
|
const node = store.findNode(id);
|
||||||
|
if (!node) return;
|
||||||
|
store.updateNode(id, {
|
||||||
|
position: {
|
||||||
|
x: node.position.x + dx,
|
||||||
|
y: node.position.y + dy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
changed();
|
||||||
|
},
|
||||||
|
undo() {
|
||||||
|
if (historyIndex <= 0) return false;
|
||||||
|
historyIndex -= 1;
|
||||||
|
const snapshot = clone(history[historyIndex].graph);
|
||||||
|
facade.render(snapshot, { resetHistory: false });
|
||||||
|
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
redo() {
|
||||||
|
if (historyIndex >= history.length - 1) return false;
|
||||||
|
historyIndex += 1;
|
||||||
|
const snapshot = clone(history[historyIndex].graph);
|
||||||
|
facade.render(snapshot, { resetHistory: false });
|
||||||
|
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
registerComponentInstance(id, instance) {
|
||||||
|
if (instance) componentInstances.set(id, instance);
|
||||||
|
},
|
||||||
|
extension: {
|
||||||
|
selectionSelect: {
|
||||||
|
openSelectionSelect: () => emit("selection:open"),
|
||||||
|
closeSelectionSelect: () => emit("selection:close"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return facade;
|
||||||
|
};
|
||||||
@ -205,7 +205,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex align-center"
|
class="flex align-center"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
@click="handleLogicFlow(item)"
|
@click="handleFlowView(item)"
|
||||||
>
|
>
|
||||||
<div>{{ item.name }}</div>
|
<div>{{ item.name }}</div>
|
||||||
<div class="flex align-center" style="margin-left: 10px">
|
<div class="flex align-center" style="margin-left: 10px">
|
||||||
@ -410,7 +410,7 @@ function handleOpenLogDialog(row) {
|
|||||||
|
|
||||||
const currentLog = ref({});
|
const currentLog = ref({});
|
||||||
|
|
||||||
const handleLogicFlow = (item) => {
|
const handleFlowView = (item) => {
|
||||||
const params = {
|
const params = {
|
||||||
instId: currentLog.value.id,
|
instId: currentLog.value.id,
|
||||||
taskId: currentLog.value.taskId,
|
taskId: currentLog.value.taskId,
|
||||||
|
|||||||
@ -400,7 +400,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex align-center"
|
class="flex align-center"
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
@click="handleLogicFlow(item)"
|
@click="handleFlowView(item)"
|
||||||
>
|
>
|
||||||
<div>{{ item.name }}</div>
|
<div>{{ item.name }}</div>
|
||||||
<div class="flex align-center" style="margin-left: 10px">
|
<div class="flex align-center" style="margin-left: 10px">
|
||||||
@ -685,7 +685,7 @@ function handleClose() {
|
|||||||
openDashboard.value = false;
|
openDashboard.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLogicFlow = (item) => {
|
const handleFlowView = (item) => {
|
||||||
const params = {
|
const params = {
|
||||||
instId: lastInstId.value,
|
instId: lastInstId.value,
|
||||||
taskId: currentLog.value.projectId,
|
taskId: currentLog.value.projectId,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user