add:增加vue-flow组件

This commit is contained in:
liangdong
2026-01-10 12:57:14 +08:00
parent 2a76877bdb
commit 23a7285e29
7 changed files with 560 additions and 0 deletions

2
components.d.ts vendored
View File

@@ -74,11 +74,13 @@ declare module 'vue' {
GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default'] GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default']
MemberSelector: typeof import('./src/components/memberSelector/index.vue')['default'] MemberSelector: typeof import('./src/components/memberSelector/index.vue')['default']
NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default'] NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default']
NodeFlow: typeof import('./src/components/nodeFlow/index.vue')['default']
OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default'] OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
PageForm: typeof import('./src/components/pageForm/index.vue')['default'] PageForm: typeof import('./src/components/pageForm/index.vue')['default']
ProTable: typeof import('./src/components/proTable/index.vue')['default'] ProTable: typeof import('./src/components/proTable/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SopNode: typeof import('./src/components/nodeFlow/sopNode.vue')['default']
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default'] StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default'] StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
StandMenu: typeof import('./src/components/standMenu/index.vue')['default'] StandMenu: typeof import('./src/components/standMenu/index.vue')['default']

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@micro-zoe/micro-app": "^1.0.0-rc.28", "@micro-zoe/micro-app": "^1.0.0-rc.28",
"@vue-flow/core": "^1.48.1",
"element-plus": "^2.13.0", "element-plus": "^2.13.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.5.24", "vue": "^3.5.24",

110
pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@micro-zoe/micro-app': '@micro-zoe/micro-app':
specifier: ^1.0.0-rc.28 specifier: ^1.0.0-rc.28
version: 1.0.0-rc.28 version: 1.0.0-rc.28
'@vue-flow/core':
specifier: ^1.48.1
version: 1.48.1(vue@3.5.26(typescript@5.9.3))
element-plus: element-plus:
specifier: ^2.13.0 specifier: ^2.13.0
version: 2.13.0(vue@3.5.26(typescript@5.9.3)) version: 2.13.0(vue@3.5.26(typescript@5.9.3))
@@ -323,36 +326,42 @@ packages:
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1': '@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1': '@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1': '@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1': '@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1': '@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1': '@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -413,56 +422,67 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0': '@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0': '@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0': '@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0': '@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0': '@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0': '@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0': '@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0': '@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0': '@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0': '@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0': '@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@@ -523,6 +543,11 @@ packages:
'@volar/typescript@2.4.27': '@volar/typescript@2.4.27':
resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==} resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==}
'@vue-flow/core@1.48.1':
resolution: {integrity: sha512-3IxaMBLvWRbznZ4CuK0kVhp4Y4lCDQx9nhi48Swp6PwPw29KNhmiKd2kaBogYeWjGLb/tLjlE9V0s3jEmKCYWw==}
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==}
@@ -636,6 +661,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'}
dayjs@1.11.19: dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
@@ -1376,6 +1439,17 @@ snapshots:
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.1.0 vscode-uri: 3.1.0
'@vue-flow/core@1.48.1(vue@3.5.26(typescript@5.9.3))':
dependencies:
'@vueuse/core': 10.11.1(vue@3.5.26(typescript@5.9.3))
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
vue: 3.5.26(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@vue/compiler-core@3.5.26': '@vue/compiler-core@3.5.26':
dependencies: dependencies:
'@babel/parser': 7.28.5 '@babel/parser': 7.28.5
@@ -1530,6 +1604,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)
dayjs@1.11.19: {} dayjs@1.11.19: {}
debug@4.4.3: debug@4.4.3:

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
import { ref } from "vue";
import { VueFlow, useVueFlow, getRectOfNodes } from "@vue-flow/core";
import sopNode from "./sopNode.vue";
const { updateNode, setViewport, nodes: flowNodes } = useVueFlow();
const nodes = ref([
{
id: "1",
type: "my-custom", // 对应插槽名 #node-my-custom
position: { x: 0, y: 0 },
label: "完成项目文档",
data: { title: "开发任务", isFinished: true, value: "", isActive: false }, // 传入自定义数据
},
{
id: "2",
type: "my-custom",
position: { x: 160 + 64, y: 0 },
label: "代码上线评审",
selected: true,
data: {
title: "代码上线评审",
isFinished: false,
value: "",
isActive: true,
},
},
{
id: "3",
type: "my-custom",
position: { x: 320 + 128, y: 0 },
label: "代码发布",
data: { title: "代码发布", value: "", isFinished: false, isActive: false },
},
{
id: "3-top",
type: "my-custom",
label: "任务完成",
position: { x: 448 + 192, y: -100 },
data: { title: "任务完成" },
}, // 向上偏
{
id: "3-bottom",
type: "my-custom",
label: "任务未完成",
position: { x: 448 + 192, y: 100 },
data: { title: "任务未完成" },
}, // 向下偏
]);
const edges = ref([
{ id: "e1-2", source: "1", target: "2", },
{ id: "e2-3", source: "2", target: "3", },
{ id: "e3-3t", source: "3", target: "3-top", },
{ id: "e3-3b", source: "3", target: "3-bottom", },
]);
const PADDING = 20;
const canvasRect = computed(() => {
// 过滤掉还没有尺寸信息的节点
const validNodes = flowNodes.value.filter((n) => n.dimensions.width > 0);
if (validNodes.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
return getRectOfNodes(validNodes);
});
const translateExtent = computed(() => {
const rect = canvasRect.value;
if (rect.width === 0)
return [
[0, 0],
[0, 0],
];
const result = [
[rect.x - PADDING, rect.y - PADDING],
[rect.x + rect.width + PADDING, rect.y + rect.height + PADDING],
];
return result;
});
const canvasStyle = computed(() => {
const bounds = canvasRect.value;
return {
// 宽度 = 节点矩形宽度 + 左右边距
width: `${bounds.width + PADDING * 2}px`,
// 高度 = 节点矩形高度 + 上下边距
height: `${bounds.height + PADDING * 2}px`,
position: "relative",
};
});
// 新增节点
const addNode = () => {
const HORIZONTAL_GAP = 40; // 缩短间距,让主干紧贴汇合点
const MAX_WIDTH = 160;
const VERTICAL_GAP = 64;
// 1. 获取所有待链接分支中,最右侧的边界
const pendingBranchNodes = nodes.value.filter(node => {
const isBranch = node.id.includes('top') || node.id.includes('bottom');
const isAlreadyLinked = edges.value.some(edge => edge.source === node.id);
return isBranch && !isAlreadyLinked;
});
let nextX = 0;
if (pendingBranchNodes.length > 0) {
// 如果有分支待汇合,新主干 X = 分支最右侧位置 + 小间距
// 注意:这里最好加上节点自身的宽度(假设是 160
const rightmostX = Math.max(...pendingBranchNodes.map(n => n.position.x));
nextX = rightmostX + MAX_WIDTH + HORIZONTAL_GAP;
} else {
// 如果没有分支,按正常主干逻辑追加
const mainNodes = nodes.value.filter(n => !n.id.includes('-'));
const lastMain = mainNodes[mainNodes.length - 1];
nextX = (lastMain?.position.x || 0) + MAX_WIDTH + VERTICAL_GAP;
}
const newId = (nodes.value.reduce((max, n) => Math.max(max, parseInt(n.id) || 0), 0) + 1).toString();
// 2. 创建新节点Y 轴保持 0主干中轴
const newNode = {
id: newId,
type: "my-custom",
position: { x: nextX, y: 0 },
label: `任务 ${newId}`,
data: { title: `任务 ${newId}`, isFinished: false, isActive: false },
};
// 3. 连线逻辑不变,但确保 type 是 smoothstep
const newEdges = [];
// 链接分支到汇合点
pendingBranchNodes.forEach(branch => {
newEdges.push({
id: `e${branch.id}-${newId}`,
source: branch.id,
target: newId,
type: "smoothstep",
// 调整 pathOptions 让折线更陡峭/紧凑,视觉上更像在“链接点处”
pathOptions: { borderRadius: 10, offset: 10 }
});
});
// 主干相连
const mainNodes = nodes.value.filter(n => !n.id.includes('-'));
if (mainNodes.length > 0) {
const prevMain = mainNodes[mainNodes.length - 1];
newEdges.push({
id: `e${prevMain.id}-${newId}`,
source: prevMain.id,
target: newId,
type: "smoothstep"
});
}
nodes.value.push(newNode);
edges.value.push(...newEdges);
};
const handleNodeClick = (clickedNodeId) => {
// 1. 先判断点击的是否是已禁用的节点,如果是则直接跳过
const targetNode = nodes.value.find((n) => n.id === clickedNodeId);
if (targetNode?.data?.isFinished) return;
// 2. 一次性映射出所有节点的新状态
nodes.value = nodes.value.map((node) => {
return {
...node,
// 只有点击的那个变 true其他所有节点强制变 false
data: {
...node.data,
isActive: node.id === clickedNodeId,
},
// 顺便同步官方的选中状态,确保视觉和逻辑统一
selected: node.id === clickedNodeId,
};
});
};
// 监听 rect 的变化(当节点增减或尺寸测量完成时触发)
watch(
() => canvasRect.value,
async (rect) => {
console.log("rect", -rect.x + PADDING, rect.width);
if (rect.width > 0) {
await nextTick();
setViewport({
x: -rect.x + PADDING,
y: -rect.y + PADDING,
zoom: 1,
});
}
},
{ deep: true }
);
// 初始化setViewport
const onInit = (instance) => {
const validNodes = flowNodes.value.filter((n) => n.dimensions.width > 0);
if (validNodes.length > 0) {
const rect = canvasRect.value;
setViewport({
x: -rect.x + PADDING,
y: -rect.y + PADDING,
zoom: 1,
});
}
};
</script>
<template>
<el-button type="primary" @click="addNode">添加节点</el-button>
<div class="scroll-container">
<el-scrollbar>
<div :style="canvasStyle">
<VueFlow
:nodes="nodes"
:edges="edges"
:fit-view-on-init="false"
:zoom-on-scroll="false"
:nodes-draggable="false"
:zoom-on-double-click="false"
:pan-on-drag="false"
:nodes-connectable="false"
:selection-key="null"
:pan-activation-action="null"
:zoom-on-pinch="false"
:min-zoom="1"
:max-zoom="1"
:translate-extent="translateExtent"
@pane-ready="onInit"
>
<template #node-my-custom="nodeProps">
<sopNode v-bind="nodeProps" @activate-node="handleNodeClick" />
</template>
</VueFlow>
</div>
</el-scrollbar>
</div>
</template>
<style>
/* 官方基础样式还是要引,不然无法拖拽和缩放 */
@import "@vue-flow/core/dist/style.css";
@import "@vue-flow/core/dist/theme-default.css";
</style>
<style lang="scss" scoped>
:deep(.vue-flow__edge-path) {
stroke: #e2e8f0;
stroke-width: 1.5;
}
</style>

View File

@@ -0,0 +1,154 @@
<script setup lang="ts">
import { Handle, Position } from "@vue-flow/core";
// 接收数据
const props = defineProps(["id","data", "label"]);
const emit = defineEmits(['activateNode']);
const updateItem = () => {
// 已经完成就不能在点击了
if(props.data.isFinished){
return;
}
emit('activateNode', props.id);
};
</script>
<template>
<div class="custom-step-node" :class="{ 'is-finished': data.isFinished }" @click="updateItem">
<div class="node-capsule" :class="{ 'is-active': data.isActive }">
<Handle type="target" :position="Position.Left" class="hidden-handle" />
<span class="status-dot"></span>
<span class="node-label">{{ data.title }}</span>
<Handle type="source" :position="Position.Right" class="hidden-handle" />
</div>
<div class="node-input-area" @click.stop>
<div class="input-wrapper-container">
<div class="input-wrapper">
<el-input type="text" v-model="data.value" @click.stop></el-input>
</div>
<span class="percent-unit">%</span>
</div>
<div class="node-tooltips">产能赋权</div>
</div>
</div>
</template>
<style scoped lang="scss">
.custom-step-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px; /* 胶囊与输入框的间距 */
width: 120px;
cursor: pointer;
}
/* 胶囊样式 */
.node-capsule {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 20px; /* 圆角胶囊 */
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
position: relative;
width: 100%;
white-space: nowrap;
transition: all 0.3s ease;
}
/* 激活状态(蓝框) */
.node-capsule.is-active {
border-color: #2563eb;
color: #2563eb;
box-shadow: 0 0 0 1px #2563eb;
}
/* 状态小圆点 */
.status-dot {
width: 6px;
height: 6px;
background-color: #d1d5db; /* 默认灰色 */
border-radius: 50%;
margin-right: 8px;
}
.is-active .status-dot {
background-color: #2563eb; /* 激活蓝色 */
}
.node-label {
font-size: 13px;
color: #4b5563;
}
.is-active .node-label {
color: #2563eb;
font-weight: 500;
}
/* 输入框区域 */
.node-input-area {
display: inline-flex;
flex-direction: column;
align-items: center;
.input-wrapper-container{
display: inline-flex;
align-items: center;
gap: 4px;
}
.node-tooltips{
color: #90a1b9;
font-size: 12px;
opacity: 0;
margin-top: 3px;
}
&:hover .node-tooltips{
opacity: 1;
}
}
.input-wrapper {
width: 60px;
:deep(.el-input__inner){
text-align: center;
font-weight: 600;
}
}
.percent-input {
width: 100%;
border: none;
text-align: center;
font-size: 14px;
font-weight: bold;
color: #1f2937;
outline: none;
}
.percent-unit {
font-size: 12px;
color: #9ca3af;
}
.is-finished {
filter: grayscale(1);
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
/* 隐藏连接点逻辑,但保持在胶囊中心高度 */
.hidden-handle {
width: 0;
height: 0;
border: none;
background-color: transparent;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<el-drawer
v-model="drawerVisible"
size="100%"
class="standard-ui-back-drawer"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
:with-header="false"
modal-class="standard-overlay-dialog-flat"
@opened="isOpened = true"
@closed="isOpened = false"
>
<!-- 头部 -->
<!-- 内容 -->
<div class="flow-context-wrapper">
<nodeFlow v-if="drawerVisible" />
</div>
<!-- -->
<template #footer>
<div class="custom-flat-drawer-footer">
<div class="stats-info"></div>
<div class="actions">
<el-button link @click="drawerVisible = false">取消</el-button>
<el-button type="primary" class="btn-confirm">确认保存变更</el-button>
</div>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import nodeFlow from "@/components/nodeFlow/index.vue";
defineOptions({ name: "FlowDetail" });
const drawerVisible = defineModel("drawerVisible", { default: true });
const isOpened = ref(false);
</script>
<style lang="scss" scoped></style>

View File

@@ -29,11 +29,14 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 详情 -->
<!-- <flow-detail /> -->
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import subTabs from "./subTabs.vue"; import subTabs from "./subTabs.vue";
import flowCard from "./flowCard.vue"; import flowCard from "./flowCard.vue";
import FlowDetail from './detail.vue';
defineOptions({ name: "Flow" }); defineOptions({ name: "Flow" });
const activeTab = ref<string>(1); const activeTab = ref<string>(1);