Compare commits
11
Commits
e30e7ab665
...
5b83fd7144
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b83fd7144 | ||
|
|
d3a2b08ba2 | ||
|
|
79866a05e1 | ||
|
|
138cb36800 | ||
|
|
ffa196954e | ||
|
|
656c0d1bf5 | ||
|
|
16717a0867 | ||
|
|
2dd7135753 | ||
|
|
e057e3bd8b | ||
|
|
678cb96801 | ||
|
|
2c65e5adc9 |
Generated
-223
@@ -15,7 +15,6 @@
|
|||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"alova": "^3.4.0",
|
"alova": "^3.4.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bpmn-js": "^17.11.1",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
@@ -296,16 +295,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bpmn-io/diagram-js-ui": {
|
|
||||||
"version": "0.2.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.4.tgz",
|
|
||||||
"integrity": "sha512-I678ZGtoH7z7FgzFhzzppM9ThsJ3Lh83kW3GjhjG3xyYciIPYHMaHK1MABP8F69H9pFZpViM6t4qUo8dwrVz0w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"htm": "^3.1.1",
|
|
||||||
"preact": "^10.29.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@braintree/sanitize-url": {
|
"node_modules/@braintree/sanitize-url": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
|
||||||
@@ -2974,36 +2963,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/bpmn-js": {
|
|
||||||
"version": "17.11.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/bpmn-js/-/bpmn-js-17.11.1.tgz",
|
|
||||||
"integrity": "sha512-ywCeTg5kvN8lYkU+fHE+YXTGlfKc55lRBn7zW3k1//toeMNPy/PS/uQiujRWdFhMrH5dbtDvlwWukNw2pjWw8Q==",
|
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
|
||||||
"dependencies": {
|
|
||||||
"bpmn-moddle": "^8.1.0",
|
|
||||||
"diagram-js": "^14.10.0",
|
|
||||||
"diagram-js-direct-editing": "^3.0.1",
|
|
||||||
"ids": "^1.0.5",
|
|
||||||
"inherits-browser": "^0.1.0",
|
|
||||||
"min-dash": "^4.1.1",
|
|
||||||
"min-dom": "^4.2.1",
|
|
||||||
"tiny-svg": "^3.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bpmn-moddle": {
|
|
||||||
"version": "8.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-8.1.0.tgz",
|
|
||||||
"integrity": "sha512-yI5OAFfYVJwViKTsTsonVfCBPtB3MlefADUORwNIxxBOMp21vnoxuxsdgUWlPH/dvAEZh/+mr8UtqOBNu8NC5Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"min-dash": "^4.0.0",
|
|
||||||
"moddle": "^6.2.3",
|
|
||||||
"moddle-xml": "^10.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
|
||||||
@@ -3306,15 +3265,6 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clsx": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/collection-visit": {
|
"node_modules/collection-visit": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/collection-visit/-/collection-visit-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/collection-visit/-/collection-visit-1.0.0.tgz",
|
||||||
@@ -3373,12 +3323,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/component-event": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/component-event/-/component-event-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/compute-scroll-into-view": {
|
"node_modules/compute-scroll-into-view": {
|
||||||
"version": "1.0.20",
|
"version": "1.0.20",
|
||||||
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||||
@@ -4325,79 +4269,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/diagram-js": {
|
|
||||||
"version": "14.11.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-14.11.3.tgz",
|
|
||||||
"integrity": "sha512-Seq9BHAXfzKS60L4v4Gvgvv72wOtvrfJQAyyPm9pntSZDMzjoodPSXnEUPud1G2zVCMGEUUW++s0reEdaWgkXA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@bpmn-io/diagram-js-ui": "^0.2.3",
|
|
||||||
"clsx": "^2.1.0",
|
|
||||||
"didi": "^10.2.2",
|
|
||||||
"inherits-browser": "^0.1.0",
|
|
||||||
"min-dash": "^4.1.0",
|
|
||||||
"min-dom": "^4.2.1",
|
|
||||||
"object-refs": "^0.4.0",
|
|
||||||
"path-intersection": "^3.0.0",
|
|
||||||
"tiny-svg": "^3.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diagram-js-direct-editing": {
|
|
||||||
"version": "3.5.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-3.5.1.tgz",
|
|
||||||
"integrity": "sha512-fEQCQ/x5oKVV0m7Nmgv7i85Vhw+kJkKNXF8zcV5Vm0KwF8x/8e3Ax0aJgdXt8/fDPymEkY8HmZweXIipCeeh+g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"min-dash": "^5.0.0",
|
|
||||||
"min-dom": "^5.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"diagram-js": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diagram-js-direct-editing/node_modules/domify": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/domify/-/domify-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-bs2yO68JDFOm6rKv8f0EnrM2cENduhRkpqOtt/s5l5JBA/eqGBZCzLPmdYoHtJ6utgLGgcBajFsEQbl12pT0lQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/diagram-js-direct-editing/node_modules/min-dash": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/min-dash/-/min-dash-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-HAvN6XzmCQj+js43G9sJRRw8kj/7DvoxN7qhoIN9Xo5ZN1NMljtpyKs/NunXmw0QSWwlgZzNv6tLeEa4xaZ0tg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/diagram-js-direct-editing/node_modules/min-dom": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-0w5FEBgPAyHhmFojW3zxd7we3D+m5XYS3E/06OyvxmbHJoiQVa4Nagj6RWvoAKYRw5xth6cP5TMePc5cR1M9hA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"domify": "^3.0.0",
|
|
||||||
"min-dash": "^5.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/didi": {
|
|
||||||
"version": "10.2.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/didi/-/didi-10.2.2.tgz",
|
|
||||||
"integrity": "sha512-l8NYkYFXV1izHI65EyT8EXOjUZtKmQkHLTT89cSP7HU5J/G7AOj0dXKtLc04EXYlga99PBY18IPjOeZ+c3DI4w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dingbat-to-unicode": {
|
"node_modules/dingbat-to-unicode": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||||
@@ -4466,15 +4337,6 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/domify": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/domify/-/domify-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.4.15",
|
"version": "3.4.15",
|
||||||
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.15.tgz",
|
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.15.tgz",
|
||||||
@@ -5660,12 +5522,6 @@
|
|||||||
"integrity": "sha512-CW/pPvSOFIRsosbwxrYaE9ERmpTo5fbTqL7wCvuCFlqW1Bmb1K5fXsY7yiH95rmlr4rVuA7UXHbM/cIXQ6AXwg==",
|
"integrity": "sha512-CW/pPvSOFIRsosbwxrYaE9ERmpTo5fbTqL7wCvuCFlqW1Bmb1K5fXsY7yiH95rmlr4rVuA7UXHbM/cIXQ6AXwg==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/htm": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/htm/-/htm-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/html-minifier-terser": {
|
"node_modules/html-minifier-terser": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
||||||
@@ -5851,12 +5707,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ids": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/ids/-/ids-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
@@ -5946,12 +5796,6 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/inherits-browser": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/inherits-browser/-/inherits-browser-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -7153,23 +6997,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/min-dash": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/min-dash/-/min-dash-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-VLMYQI5+FcD9Ad24VcB08uA83B07OhueAlZ88jBK6PyupTvEJwllTMUqMy0wPGYs7pZUEtEEMWdHB63m3LtEcg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/min-dom": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"component-event": "^0.2.1",
|
|
||||||
"domify": "^1.4.1",
|
|
||||||
"min-dash": "^4.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||||
@@ -7236,26 +7063,6 @@
|
|||||||
"random": "bin/random"
|
"random": "bin/random"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/moddle": {
|
|
||||||
"version": "6.2.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/moddle/-/moddle-6.2.3.tgz",
|
|
||||||
"integrity": "sha512-bLVN+ZHL3aKnhxc19XtjUfvdJsS3EsiEJC7bT6YPD11qYmTzvsxrGgyYz1Ouof7TZuGw0lDJ1OLmEnxcpQWk3Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"min-dash": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moddle-xml": {
|
|
||||||
"version": "10.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/moddle-xml/-/moddle-xml-10.1.0.tgz",
|
|
||||||
"integrity": "sha512-erWckwLt+dYskewKXJso9u+aAZ5172lOiYxSOqKCPTy7L/xmqH1PoeoA7eVC7oJTt3PqF5TkZzUmbjGH6soQBg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"min-dash": "^4.0.0",
|
|
||||||
"moddle": "^6.0.0",
|
|
||||||
"saxen": "^8.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
|
||||||
@@ -7624,15 +7431,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-refs": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/object-refs/-/object-refs-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-visit": {
|
"node_modules/object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/object-visit/-/object-visit-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/object-visit/-/object-visit-1.0.1.tgz",
|
||||||
@@ -7929,15 +7727,6 @@
|
|||||||
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-intersection": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/path-intersection/-/path-intersection-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-3xS3lvv/vuwm5aH2BVvNRvnvwR2Drde7jQClKpCXTYXIMMjcw/EnMhzCgeHwqbCpzi760PEfAkU53vSIlrNr9A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-is-absolute": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
@@ -8860,12 +8649,6 @@
|
|||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/saxen": {
|
|
||||||
"version": "8.1.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz",
|
|
||||||
"integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/scroll-into-view-if-needed": {
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
"version": "2.2.31",
|
"version": "2.2.31",
|
||||||
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||||
@@ -9875,12 +9658,6 @@
|
|||||||
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
|
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tiny-svg": {
|
|
||||||
"version": "3.1.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tiny-svg/-/tiny-svg-3.1.3.tgz",
|
|
||||||
"integrity": "sha512-9mwnPqXInRsBmH/DO6NMxBE++9LsqpVXQSSTZGc5bomoKKvL5OX/Hlotw7XVXP6XLRcHWIzZpxfovGqWKgCypQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tiny-warning": {
|
"node_modules/tiny-warning": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"alova": "^3.4.0",
|
"alova": "^3.4.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"bpmn-js": "^17.11.1",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"mockjs": "^1.1.0",
|
"mockjs": "^1.1.0",
|
||||||
|
|||||||
+16
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from "vue";
|
import { ref, reactive, computed, onMounted } from "vue";
|
||||||
import layout from "./components/layout.vue";
|
import layout from "./components/layout.vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import config from "@/assets/config/host.config";
|
import config from "@/assets/config/host.config";
|
||||||
@@ -9,6 +9,9 @@ import { Message } from "view-ui-plus";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isLogin = ref(false);
|
const isLogin = ref(false);
|
||||||
|
// 嵌入页旁路(/embed/**,iframe 免签嵌入):子应用 Drawer 里打开,带不了本应用 CAS 会话,
|
||||||
|
// 也不该出现菜单壳。判定用路由前缀而非 query 标记——刷新/直链/分享均稳定
|
||||||
|
const isEmbed = computed(() => router.currentRoute.value.path.startsWith("/embed/"));
|
||||||
const layoutData = reactive({
|
const layoutData = reactive({
|
||||||
appName: "流程中心",
|
appName: "流程中心",
|
||||||
baseHost: config.baseHost,
|
baseHost: config.baseHost,
|
||||||
@@ -20,6 +23,14 @@ const layoutData = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 首次导航异步完成:mount 时 currentRoute 可能仍是初始 "/",先等路由就绪再判嵌入前缀,
|
||||||
|
// 否则直链 /#/embed/* 会在旁路前误发鉴权请求
|
||||||
|
await router.isReady();
|
||||||
|
// 嵌入页:跳过两个鉴权请求(失败会弹错误 Message 且渲染带菜单壳的 layout,嵌入场景均不可接受)
|
||||||
|
if (isEmbed.value) {
|
||||||
|
isLogin.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// 1. 获取当前用户信息
|
// 1. 获取当前用户信息
|
||||||
const user: any = await baseApi.getUserInfo().send();
|
const user: any = await baseApi.getUserInfo().send();
|
||||||
@@ -75,12 +86,16 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 嵌入页:裸渲染,不套 layout 与 base-wrap-main(嵌入页自己管理样式与滚动) -->
|
||||||
|
<router-view v-if="isEmbed" />
|
||||||
|
<template v-else>
|
||||||
<layout v-if="isLogin" :layoutData="layoutData" />
|
<layout v-if="isLogin" :layoutData="layoutData" />
|
||||||
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
<div v-if="isLogin" class="base-wrap-main" :class="{ 'alone-page': layoutData.isAlone }">
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("@/views/flow/admin/Routers.vue"),
|
component: () => import("@/views/flow/admin/Routers.vue"),
|
||||||
meta: { title: "条件路由", modId: 106 },
|
meta: { title: "条件路由", modId: 106 },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 办件进度嵌入页(iframe 免签嵌入,App.vue 对 /embed/* 前缀旁路 CAS 与 layout):
|
||||||
|
// 不挂菜单、无 parentId(子应用 Drawer 内打开,非管理面导航);query.token 必填
|
||||||
|
path: "/embed/instance",
|
||||||
|
name: "flow_embed_instance",
|
||||||
|
component: () => import("@/views/flow/EmbedInstance.vue"),
|
||||||
|
meta: { title: "办件进度" },
|
||||||
|
},
|
||||||
{ path: "/", redirect: "/workbench" },
|
{ path: "/", redirect: "/workbench" },
|
||||||
// 兜底:UserMsg jumpPath / bizUrl 等外部深链若落到未登记路由,避免 no-match 静默空白页
|
// 兜底:UserMsg jumpPath / bizUrl 等外部深链若落到未登记路由,避免 no-match 静默空白页
|
||||||
{ path: "/:pathMatch(.*)*", redirect: "/workbench" },
|
{ path: "/:pathMatch(.*)*", redirect: "/workbench" },
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// 办件进度嵌入页(iframe 免签嵌入,2026-10-03 方案):子应用 Drawer 里以 iframe 打开本页,
|
||||||
|
// 展示"进度 + 流程示意图",零样式复制、单点维护。全程零 CAS 交互——数据走 /open/flow/embed/data
|
||||||
|
// 免签端点(只认 token),示意图 token 两用(/open/flow/diagram/img 同一 token 出图)。
|
||||||
|
// 本页只读(方案 §9):不做任何审批动作;办理入口仍是待办站内信 → /task/:taskId 既有链路。
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { Tag } from "view-ui-plus";
|
||||||
|
import { INSTANCE_STATUS, INSTANCE_STATUS_COLOR, TASK_STATUS } from "@/api/module/flow";
|
||||||
|
import TraceTimeline from "@/views/flow/components/TraceTimeline.vue";
|
||||||
|
import config from "@/assets/config/host.config";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const detail = ref<any>(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
/** 兜底空态:缺 token / 过期 / 加载失败,全页同一文案,不留白 */
|
||||||
|
const expired = ref(false);
|
||||||
|
|
||||||
|
/** 显示口径:dept=按科室 / user=按人员(缺省)。纯前端本地状态——嵌入页 URL 归子应用管,
|
||||||
|
* 改 query 会破坏 token 语义,不落 URL(与管理面 InstanceDetail 写 query 的动机不同) */
|
||||||
|
const traceMode = ref<string>("user");
|
||||||
|
function switchMode(val: string) {
|
||||||
|
traceMode.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = computed(() => String(route.query.token || ""));
|
||||||
|
|
||||||
|
/** 示意图直嵌:同一 token 第二处消费(换图端点零改动即认领嵌入 token) */
|
||||||
|
const diagramSrc = computed(
|
||||||
|
() => `${config.serviceHost}/open/flow/diagram/img?token=${token.value}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotRows = computed(() => {
|
||||||
|
const raw = detail.value?.bizSnapshot;
|
||||||
|
if (!raw) return [];
|
||||||
|
let obj: any = null;
|
||||||
|
try {
|
||||||
|
obj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||||
|
} catch {
|
||||||
|
return [{ k: "摘要", v: String(raw) }];
|
||||||
|
}
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
return Object.entries(obj).map(([k, v]) => ({ k, v: v == null ? "-" : String(v) }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 业务表单数据键值平铺(同 InstanceDetail 现状,不做精细表单渲染) */
|
||||||
|
const formRows = computed(() => {
|
||||||
|
const raw = detail.value?.formDataJson;
|
||||||
|
if (raw === null || raw === undefined || raw === "") return [];
|
||||||
|
let obj: any = null;
|
||||||
|
try {
|
||||||
|
obj = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||||
|
} catch {
|
||||||
|
return [{ k: "表单原文", v: String(raw) }];
|
||||||
|
}
|
||||||
|
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
||||||
|
return Object.entries(obj).map(([k, v]) => ({
|
||||||
|
k,
|
||||||
|
v: v == null ? "-"
|
||||||
|
: Array.isArray(v)
|
||||||
|
? (v.map((item: any) => item && typeof item === "object" ? item.fileName || JSON.stringify(item) : String(item)).join("、") || "-")
|
||||||
|
: (typeof v === "object" ? JSON.stringify(v) : String(v)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [{ k: "表单原文", v: String(raw) }];
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 审批记录流水表(整个流程周期,与"流程轨迹"时间线互补:时间线给进度感,表格给逐条可查流水) =====
|
||||||
|
|
||||||
|
/** 事件类型 → 动作标签主题色(逐字照抄 TraceTimeline.vue 的 KIND_TAG:两处口径,改一处必须同步另一处) */
|
||||||
|
const RECORD_KIND_TAG: Record<string, string> = {
|
||||||
|
SUBMIT: "green",
|
||||||
|
RESUBMIT: "green",
|
||||||
|
WITHDRAW: "orange",
|
||||||
|
APPROVE: "green",
|
||||||
|
REJECT: "red",
|
||||||
|
RETURN: "orange",
|
||||||
|
TRANSFER: "cyan",
|
||||||
|
INVALID: "default",
|
||||||
|
PENDING: "blue",
|
||||||
|
CC: "cyan",
|
||||||
|
ROUTE: "purple",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 多轮才显示"轮次"列(单轮不打扰,口径同 TraceTimeline 的 multiRound) */
|
||||||
|
const multiRound = computed(() =>
|
||||||
|
(detail.value?.trace || []).some((t: any) => (t.round || 1) > 1));
|
||||||
|
|
||||||
|
function fmtTime(v: string | null | undefined) {
|
||||||
|
return v ? String(v).replace("T", " ").replace(/\.\d+$/, "") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 操作者(口径=TraceTimeline.actor:dept 档不泄露人名,科室节点显科室、指定人显"指定办理人") */
|
||||||
|
function recordActor(t: any): string {
|
||||||
|
if (t.kind === "ROUTE") return "流程引擎";
|
||||||
|
if (traceMode.value !== "dept") {
|
||||||
|
return t.assigneeName || t.assigneeId || "-";
|
||||||
|
}
|
||||||
|
if (t.assigneeType === "DEPT_KEY") {
|
||||||
|
return t.deptName || t.deptKey || t.assigneeName || "-";
|
||||||
|
}
|
||||||
|
if (t.assigneeType === "USERS") {
|
||||||
|
return "指定办理人";
|
||||||
|
}
|
||||||
|
return t.kind === "SUBMIT" || t.kind === "RESUBMIT" || t.kind === "WITHDRAW"
|
||||||
|
? "提交人"
|
||||||
|
: (t.assigneeName || "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 动作标签(口径=TraceTimeline.actionText:CC/ROUTE 专用文案,任务行走 TASK_STATUS 码表) */
|
||||||
|
function recordAction(t: any): string {
|
||||||
|
if (t.kind === "CC") return "已抄送";
|
||||||
|
if (t.kind === "ROUTE") return "系统判断";
|
||||||
|
return TASK_STATUS[t.status] || String(t.status ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格列:Drawer iframe 宽约 828px,列宽按此设计;轮次列仅多轮时出现 */
|
||||||
|
const recordColumns = computed(() => {
|
||||||
|
const cols: any[] = [
|
||||||
|
{ title: "时间", width: 150, render: (h: any, p: any) => h("span", fmtTime(p.row.time)) },
|
||||||
|
];
|
||||||
|
if (multiRound.value) {
|
||||||
|
cols.push({
|
||||||
|
title: "轮次", width: 80, align: "center",
|
||||||
|
render: (h: any, p: any) => h("span", `第 ${p.row.round || 1} 轮`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
title: "节点", minWidth: 120,
|
||||||
|
render: (h: any, p: any) => h("span", p.row.nodeName || p.row.nodeId || "-"),
|
||||||
|
},
|
||||||
|
{ title: "办理人", width: 110, render: (h: any, p: any) => h("span", recordActor(p.row)) },
|
||||||
|
{
|
||||||
|
title: "动作", width: 90, align: "center",
|
||||||
|
render: (h: any, p: any) =>
|
||||||
|
h(Tag, { size: "small", color: RECORD_KIND_TAG[p.row.kind] || "default" },
|
||||||
|
() => recordAction(p.row)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "意见 / 去向", minWidth: 180,
|
||||||
|
render: (h: any, p: any) => {
|
||||||
|
const parts: any[] = [];
|
||||||
|
if (p.row.opinion) parts.push(h("span", `「${p.row.opinion}」`));
|
||||||
|
if (p.row.edgeName) parts.push(h("span", `去向:${p.row.edgeName}`));
|
||||||
|
if (p.row.kind === "ROUTE") {
|
||||||
|
// outlet 空=判断失败挂起(同 TraceTimeline 的 route-hung 文案),等管理端重跑路由
|
||||||
|
parts.push(p.row.outlet
|
||||||
|
? h("span", `→ ${p.row.outlet}`)
|
||||||
|
: h("span", { style: "color:#ed4014" }, `判断失败已挂起(第 ${p.row.attempt || 1} 次)`));
|
||||||
|
}
|
||||||
|
return parts.length ? h("span", parts) : h("span", { style: "color:#999" }, "-");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return cols;
|
||||||
|
});
|
||||||
|
|
||||||
|
function statusName(s: number) {
|
||||||
|
return INSTANCE_STATUS[s] || s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(s: number) {
|
||||||
|
return INSTANCE_STATUS_COLOR[s] || "gray";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 免签端点不带任何会话,alova 拦截器(token 头/信封解析)一概用不上,原生 fetch 直取
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!token.value) {
|
||||||
|
expired.value = true;
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${config.serviceHost}/open/flow/embed/data?token=${token.value}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
// 404=过期/伪造,与缺 token 同空态(不区分,不诱导探测)
|
||||||
|
expired.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detail.value = await res.json();
|
||||||
|
} catch {
|
||||||
|
expired.value = true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="embed-page" v-if="detail">
|
||||||
|
<div class="head">
|
||||||
|
<span class="title">{{ detail.bizTitle }}</span>
|
||||||
|
<Tag :color="statusColor(detail.status)">{{ statusName(detail.status) }}</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card class="block" :bordered="false" dis-hover>
|
||||||
|
<template #title>基本信息</template>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="row"><span class="k">流程</span><span>{{ detail.defCode }} v{{ detail.defVersion }}</span></div>
|
||||||
|
<div class="row"><span class="k">提交人</span><span>{{ detail.submitterName }}</span></div>
|
||||||
|
<div class="row"><span class="k">提交时间</span><span>{{ (detail.submitTime || "").replace("T", " ") }}</span></div>
|
||||||
|
<div class="row"><span class="k">办结时间</span><span>{{ (detail.finishTime || "").replace("T", " ") || "-" }}</span></div>
|
||||||
|
</div>
|
||||||
|
<template v-if="snapshotRows.length">
|
||||||
|
<Divider size="small" />
|
||||||
|
<div class="snapshot">
|
||||||
|
<div v-for="row in snapshotRows" :key="row.k" class="row">
|
||||||
|
<span class="k">{{ row.k }}</span>
|
||||||
|
<span class="v">{{ row.v }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="block" :bordered="false" dis-hover>
|
||||||
|
<template #title>流程示意图</template>
|
||||||
|
<img class="diagram-img" :src="diagramSrc" alt="流程示意图" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="block" :bordered="false" dis-hover>
|
||||||
|
<template #title>
|
||||||
|
流程轨迹
|
||||||
|
<RadioGroup class="trace-mode" :model-value="traceMode" size="small" type="button" @on-change="switchMode">
|
||||||
|
<Radio label="user">按人员</Radio>
|
||||||
|
<Radio label="dept">按科室</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
</template>
|
||||||
|
<TraceTimeline :trace="detail.trace || []" :mode="traceMode" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="block" :bordered="false" dis-hover>
|
||||||
|
<template #title>
|
||||||
|
审批记录
|
||||||
|
<span class="record-tip">整个流程周期的完整事件流水</span>
|
||||||
|
</template>
|
||||||
|
<Table size="small" :columns="recordColumns" :data="detail.trace || []" />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="block" :bordered="false" dis-hover v-if="formRows.length">
|
||||||
|
<template #title>业务表单数据</template>
|
||||||
|
<div class="snapshot">
|
||||||
|
<div v-for="row in formRows" :key="row.k" class="row">
|
||||||
|
<span class="k">{{ row.k }}</span>
|
||||||
|
<span class="v">{{ row.v }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loading" class="embed-page muted">加载中…</div>
|
||||||
|
<div v-else class="embed-page muted expired">链接已过期,请回到来源应用重新打开</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 根类不用 .page:UnoCSS 原子类同名会以更高优先级盖掉 scoped 的 padding(裸渲染无 layout 壳时暴露) */
|
||||||
|
.embed-page { padding: 12px 16px; }
|
||||||
|
.head { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||||
|
.title { font-size: 16px; font-weight: 600; }
|
||||||
|
.block { margin-bottom: 12px; }
|
||||||
|
.grid .row { display: flex; padding: 3px 0; }
|
||||||
|
.k { width: 120px; color: #888; flex-shrink: 0; }
|
||||||
|
.v { white-space: pre-wrap; word-break: break-all; }
|
||||||
|
.muted { color: #999; }
|
||||||
|
.expired { padding: 48px 16px; text-align: center; }
|
||||||
|
.diagram-img { max-width: 100%; }
|
||||||
|
.trace-mode { float: right; font-weight: normal; margin-right: 16px; }
|
||||||
|
/* 类名带 record- 前缀:防 UnoCSS 原子类撞名(嵌入页裸渲染无 layout 壳,撞名实测会盖样式) */
|
||||||
|
.record-tip {
|
||||||
|
float: right;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-right: 16px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+16
-121
@@ -1,20 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 流程示意图(方案 §4):深链页不挂菜单;query: flowGuid|instanceId|defId(三选一)+ mode(dept|user,缺省 user)。
|
// 流程示意图(方案 §4):深链页不挂菜单;query: flowGuid|instanceId|defId(三选一)+ mode(dept|user,缺省 user)。
|
||||||
// 三视图(2026-09-17):auto=示意图(手写 SVG 自动布局,零图形库)|canvas=画布视图(bpmn-js 只读渲染,
|
// 出图形态两档(2026-10-03 画布视图退役):auto=示意图(后端 positions 单源,手写 SVG)|img=进度图(服务端 PNG 流)。
|
||||||
// 复用设计器的 contentJson→BPMN 通道,与示意图视觉区分)|img=进度图(服务端 PNG 流)。
|
|
||||||
// 显示口径(2026-09-18):只两档 dept=科室/user=人员,不传=人员;与办件详情/办理页的轨迹口径同参数。
|
// 显示口径(2026-09-18):只两档 dept=科室/user=人员,不传=人员;与办件详情/办理页的轨迹口径同参数。
|
||||||
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from "vue";
|
import { ref, computed, onMounted, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import { flowApi } from "@/api/module/flow";
|
import { flowApi } from "@/api/module/flow";
|
||||||
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
|
import SourceDrawer from "@/views/flow/components/SourceDrawer.vue";
|
||||||
import config from "@/assets/config/host.config";
|
import config from "@/assets/config/host.config";
|
||||||
import NavigatedViewer from "bpmn-js/lib/NavigatedViewer";
|
|
||||||
import { toBpmnXml, CanvasJson } from "@/views/flow/admin/def-editor/bpmn/serialize";
|
|
||||||
// bpmn-js 渲染样式(设计器 custom.ts 已全局引过,此处再引由打包器去重;漏引连线/图标异常)
|
|
||||||
import "diagram-js/assets/diagram-js.css";
|
|
||||||
import "bpmn-js/dist/assets/bpmn-js.css";
|
|
||||||
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -29,10 +22,9 @@ const sourceText = ref("");
|
|||||||
const sourceTitle = ref("");
|
const sourceTitle = ref("");
|
||||||
const sourceLoading = ref(false);
|
const sourceLoading = ref(false);
|
||||||
|
|
||||||
/** 出图形态(2026-09-17 三选一):auto=示意图(自动布局)|canvas=画布视图(bpmn-js 只读)|img=进度图(服务端 PNG 流) */
|
/** 出图形态两档(2026-10-03 砍画布视图):auto=示意图|img=进度图;旧深链 chart=canvas 归一到 auto */
|
||||||
const chartStyle = ref<string>((route.query.chart as string) || "auto");
|
const chartStyle = ref<string>(route.query.chart === "img" ? "img" : "auto");
|
||||||
const showImg = computed(() => chartStyle.value === "img");
|
const showImg = computed(() => chartStyle.value === "img");
|
||||||
const showCanvas = computed(() => chartStyle.value === "canvas");
|
|
||||||
|
|
||||||
/** 进度图 img 直嵌源(同 InstanceDetail 的"CAS 验证码式",img 不走 alova 显式带 serviceHost;管理面 defId 走 admin 端点) */
|
/** 进度图 img 直嵌源(同 InstanceDetail 的"CAS 验证码式",img 不走 alova 显式带 serviceHost;管理面 defId 走 admin 端点) */
|
||||||
const pngSrc = computed(() => {
|
const pngSrc = computed(() => {
|
||||||
@@ -49,8 +41,18 @@ const DEF_STATUS: Record<number, string> = { 0: "草稿", 1: "已发布", 2: "
|
|||||||
// 布局常量与拓扑分层算法在 src/utils/flowLayout.ts(与设计器画布共用;并行分支组垂直居中)
|
// 布局常量与拓扑分层算法在 src/utils/flowLayout.ts(与设计器画布共用;并行分支组垂直居中)
|
||||||
import { layout as autoLayout, PAD, Pos } from "@/utils/flowLayout";
|
import { layout as autoLayout, PAD, Pos } from "@/utils/flowLayout";
|
||||||
|
|
||||||
|
/** 布局:后端 positions 单源(内容区坐标);旧后端未下发时回退本地自动布局(灰度兜底) */
|
||||||
const positions = computed<Record<string, Pos>>(() => {
|
const positions = computed<Record<string, Pos>>(() => {
|
||||||
return autoLayout({ nodes: vo.value?.nodes || [], edges: vo.value?.edges || [] });
|
const raw = (vo.value as any)?.positions as Record<string, { x: number; y: number; w: number; h: number }> | undefined;
|
||||||
|
const nodes: any[] = vo.value?.nodes || [];
|
||||||
|
if (raw && nodes.length && nodes.every((n) => raw[String(n.id)])) {
|
||||||
|
const out: Record<string, Pos> = {};
|
||||||
|
Object.entries(raw).forEach(([id, p]) => {
|
||||||
|
out[id] = { x: p.x, y: p.y, w: p.w, h: p.h, depth: 0, row: 0 };
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return autoLayout({ nodes, edges: vo.value?.edges || [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
const vbSize = computed(() => {
|
const vbSize = computed(() => {
|
||||||
@@ -178,94 +180,10 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 画布视图(bpmn-js 只读渲染,2026-09-17 与示意图区分) ====================
|
|
||||||
|
|
||||||
const bpmnHost = ref<HTMLDivElement | null>(null);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let bpmnViewer: any = null;
|
|
||||||
|
|
||||||
/** DiagramVo → 画布视图数据(layout 为后端透传的设计器中心坐标;缺省回退共享自动布局) */
|
|
||||||
const bpmnDef = computed<CanvasJson | null>(() => {
|
|
||||||
const nodes: any[] = vo.value?.nodes || [];
|
|
||||||
if (!nodes.length) return null;
|
|
||||||
return {
|
|
||||||
code: "",
|
|
||||||
name: vo.value?.name || "",
|
|
||||||
start: "start",
|
|
||||||
nodes: nodes.map((n) => ({
|
|
||||||
id: String(n.id),
|
|
||||||
type: n.type,
|
|
||||||
name: n.subLabel ? `${n.label}\n${n.subLabel}` : n.label,
|
|
||||||
})),
|
|
||||||
edges: ((vo.value?.edges || []) as any[]).map((e) => ({
|
|
||||||
from: String(e.from), to: String(e.to), name: e.name,
|
|
||||||
})),
|
|
||||||
settings: {},
|
|
||||||
layout: vo.value?.layout as CanvasJson["layout"],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
async function initBpmn() {
|
|
||||||
const host = bpmnHost.value;
|
|
||||||
const def = bpmnDef.value;
|
|
||||||
if (!host || !def) return;
|
|
||||||
bpmnViewer?.destroy();
|
|
||||||
bpmnViewer = new NavigatedViewer({ container: host });
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
await (bpmnViewer as any).importXML(toBpmnXml(def));
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(`画布视图载入失败:${e?.message || e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const canvas = bpmnViewer.get("canvas") as any;
|
|
||||||
canvas.zoom("fit-viewport", "auto");
|
|
||||||
// 运行时状态着色(instance 口径):marker 类 + <style> 里 .fc-* 规则
|
|
||||||
nodesWithState().forEach(([id, state]) => canvas.addMarker(id, `fc-${state}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodesWithState(): Array<[string, string]> {
|
|
||||||
return ((vo.value?.nodes || []) as any[])
|
|
||||||
.filter((n) => n.state)
|
|
||||||
.map((n) => [String(n.id), String(n.state)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([showCanvas, bpmnDef], ([on]) => {
|
|
||||||
if (on) void nextTick(initBpmn);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
bpmnViewer?.destroy();
|
|
||||||
bpmnViewer = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function bpmnZoom(delta: number) {
|
|
||||||
if (!bpmnViewer) return;
|
|
||||||
const c = bpmnViewer.get("canvas");
|
|
||||||
c.zoom(c.zoom() + delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bpmnFit() {
|
|
||||||
bpmnViewer?.get("canvas").zoom("fit-viewport", "auto");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 导出 PNG(方案 §4.4) ====================
|
// ==================== 导出 PNG(方案 §4.4) ====================
|
||||||
|
|
||||||
/** 当前形态的 SVG 源:示意图取 DOM,画布视图取 bpmn saveSVG(补 width/height 供 Image 取自然尺寸) */
|
/** 当前形态的 SVG 源:示意图取 DOM */
|
||||||
async function currentSvg(): Promise<string | null> {
|
async function currentSvg(): Promise<string | null> {
|
||||||
if (chartStyle.value === "canvas") {
|
|
||||||
if (!bpmnViewer) return null;
|
|
||||||
const { svg } = await bpmnViewer.saveSVG();
|
|
||||||
if (!/width="/.test(svg)) {
|
|
||||||
const m = svg.match(/viewBox="([\d.\s-]+)"/);
|
|
||||||
if (m) {
|
|
||||||
const parts = m[1].trim().split(/\s+/);
|
|
||||||
return svg.replace(/<svg /, `<svg width="${parts[2]}" height="${parts[3]}" `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
if (!svgRef.value) return null;
|
if (!svgRef.value) return null;
|
||||||
return new XMLSerializer().serializeToString(svgRef.value);
|
return new XMLSerializer().serializeToString(svgRef.value);
|
||||||
}
|
}
|
||||||
@@ -330,7 +248,6 @@ watch(() => route.query, () => load());
|
|||||||
{{ DEF_STATUS[vo.status] || vo.status }}</Tag>
|
{{ DEF_STATUS[vo.status] || vo.status }}</Tag>
|
||||||
<RadioGroup :model-value="chartStyle" size="small" type="button" @on-change="switchChartStyle">
|
<RadioGroup :model-value="chartStyle" size="small" type="button" @on-change="switchChartStyle">
|
||||||
<Radio label="auto">示意图</Radio>
|
<Radio label="auto">示意图</Radio>
|
||||||
<Radio label="canvas">画布视图</Radio>
|
|
||||||
<Radio label="img">进度图</Radio>
|
<Radio label="img">进度图</Radio>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<RadioGroup v-if="!showImg" :model-value="viewMode" size="small" @on-change="switchMode">
|
<RadioGroup v-if="!showImg" :model-value="viewMode" size="small" @on-change="switchMode">
|
||||||
@@ -346,15 +263,6 @@ watch(() => route.query, () => load());
|
|||||||
<div class="canvas img-box" v-if="vo && showImg">
|
<div class="canvas img-box" v-if="vo && showImg">
|
||||||
<img :src="pngSrc" alt="流程示意图" />
|
<img :src="pngSrc" alt="流程示意图" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 画布视图:bpmn-js 只读渲染(bpmn.io 水印为协议要求,勿删) -->
|
|
||||||
<div class="canvas bpmn-box" v-else-if="vo && showCanvas">
|
|
||||||
<div ref="bpmnHost" class="bpmn-host"></div>
|
|
||||||
<div class="bpmn-ops">
|
|
||||||
<Button size="small" @click="bpmnZoom(-0.1)">−</Button>
|
|
||||||
<Button size="small" @click="bpmnFit">复位</Button>
|
|
||||||
<Button size="small" @click="bpmnZoom(0.1)">+</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 示意图:手写 SVG 自动布局 -->
|
<!-- 示意图:手写 SVG 自动布局 -->
|
||||||
<div class="canvas" v-else-if="vo">
|
<div class="canvas" v-else-if="vo">
|
||||||
<svg :width="vbSize.w" :height="vbSize.h" :viewBox="`0 0 ${vbSize.w} ${vbSize.h}`"
|
<svg :width="vbSize.w" :height="vbSize.h" :viewBox="`0 0 ${vbSize.w} ${vbSize.h}`"
|
||||||
@@ -473,17 +381,4 @@ watch(() => route.query, () => load());
|
|||||||
0%, 100% { filter: none; }
|
0%, 100% { filter: none; }
|
||||||
50% { filter: drop-shadow(0 0 6px rgba(237, 64, 20, 0.8)); }
|
50% { filter: drop-shadow(0 0 6px rgba(237, 64, 20, 0.8)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 画布视图 */
|
|
||||||
.bpmn-box { position: relative; }
|
|
||||||
.bpmn-host { height: 560px; }
|
|
||||||
.bpmn-ops { position: absolute; top: 10px; right: 12px; display: flex; gap: 6px; z-index: 5; }
|
|
||||||
/* bpmn.io 水印按协议保留 */
|
|
||||||
.bpmn-box :deep(.bjs-powered-by) { display: block; }
|
|
||||||
/* 运行时状态着色(instance 口径 marker:done/active/rejected/pending) */
|
|
||||||
.bpmn-box :deep(.fc-done > .djs-visual > :nth-child(1)) { stroke: #19be6b !important; }
|
|
||||||
.bpmn-box :deep(.fc-active > .djs-visual > :nth-child(1)) { stroke: #ed4014 !important; stroke-width: 2.5px !important; }
|
|
||||||
.bpmn-box :deep(.fc-rejected > .djs-visual > :nth-child(1)) { stroke: #ed4014 !important; fill: #fff1f0 !important; }
|
|
||||||
.bpmn-box :deep(.fc-pending > .djs-visual > :nth-child(1)) { stroke: #b7bcc4 !important; }
|
|
||||||
.bpmn-box :deep(.fc-done > .djs-visual > .djs-label) { fill: #4f5b66; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
/**
|
|
||||||
* bpmn-js 定制模块:调色板/上下文垫收敛为 4 类业务节点 + 汉化。
|
|
||||||
* 拖拽/多选/撤销重做/缩放平移均为 bpmn-js 内建能力,此处无需实现。
|
|
||||||
* 调色板与上下文垫只暴露业务语义的 5 类元素(开始/结束/审批/并行分发/并行合并用同一 ParallelGateway,
|
|
||||||
* split/join 由 serialize.ts 以扩展属性 flowNodeType 显式标记),其余 BPMN 形状对管理员隐藏。
|
|
||||||
* bpmn.io 水印为协议要求,不做隐藏。
|
|
||||||
*/
|
|
||||||
import Modeler from "bpmn-js/lib/Modeler";
|
|
||||||
import PaletteProvider from "bpmn-js/lib/features/palette/PaletteProvider";
|
|
||||||
// 必需:diagram-js 画布/连线/调色板样式 + bpmn 字体图标;漏引会导致连线变黑条、黑锚点外露、左侧工具栏不可见
|
|
||||||
import "diagram-js/assets/diagram-js.css";
|
|
||||||
import "bpmn-js/dist/assets/bpmn-js.css";
|
|
||||||
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";
|
|
||||||
|
|
||||||
/** 汉化词表(palette/contextPad/常用按钮;未命中键原样透回) */
|
|
||||||
const TRANSLATIONS: Record<string, string> = {
|
|
||||||
"Create StartEvent": "创建开始",
|
|
||||||
"Create EndEvent": "创建结束",
|
|
||||||
"Create Task": "创建审批节点",
|
|
||||||
"Create User Task": "创建审批节点",
|
|
||||||
"Create Service Task": "创建抄送节点",
|
|
||||||
"Create Gateway": "创建并行分支",
|
|
||||||
"Create Gateways": "创建并行分支",
|
|
||||||
"Create ParallelGateway": "创建并行分支",
|
|
||||||
"Create expanded ParallelGateway": "创建并行分支",
|
|
||||||
"Create ExclusiveGateway": "创建条件分支",
|
|
||||||
"Create expanded ExclusiveGateway": "创建条件分支",
|
|
||||||
"Append anything": "追加",
|
|
||||||
"Change type": "变更类型",
|
|
||||||
"Remove element": "删除",
|
|
||||||
Connect: "连线",
|
|
||||||
"Connect to other element": "连线到其他元素",
|
|
||||||
"Connect using Association/Sequence/DataAssociation": "连线",
|
|
||||||
"Activate the global connect tool": "激活全局连线工具",
|
|
||||||
"Activate the create/remove space tool": "激活空间调整工具",
|
|
||||||
"Activate the lasso tool": "激活套索选择",
|
|
||||||
"Activate the hand tool": "激活平移",
|
|
||||||
"Activate the direct editing tool": "激活文本编辑",
|
|
||||||
undo: "撤销",
|
|
||||||
redo: "重做",
|
|
||||||
};
|
|
||||||
|
|
||||||
function createTranslateModule() {
|
|
||||||
return {
|
|
||||||
translate: [
|
|
||||||
"value",
|
|
||||||
(template: string, replacements?: Record<string, string>) => {
|
|
||||||
let t = TRANSLATIONS[template] || template;
|
|
||||||
if (replacements) {
|
|
||||||
for (const [k, v] of Object.entries(replacements)) {
|
|
||||||
t = t.replace(`{${k}}`, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return t;
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 调色板收敛:仅连线工具 + 4 类节点 */
|
|
||||||
function createPaletteModule() {
|
|
||||||
function CustomPaletteProvider(this: unknown, palette: unknown, create: unknown, elementFactory: unknown,
|
|
||||||
globalConnect: unknown, translate: unknown) {
|
|
||||||
// @ts-expect-error 继承自 bpmn-js 内部父类(js 库,无类型面板)
|
|
||||||
PaletteProvider.call(this, palette, create, elementFactory, globalConnect, translate);
|
|
||||||
this.create = create;
|
|
||||||
this.elementFactory = elementFactory;
|
|
||||||
this.globalConnect = globalConnect;
|
|
||||||
this.translate = translate;
|
|
||||||
}
|
|
||||||
CustomPaletteProvider.$inject = ["palette", "create", "elementFactory", "globalConnect", "translate"];
|
|
||||||
CustomPaletteProvider.prototype = Object.create(PaletteProvider.prototype);
|
|
||||||
CustomPaletteProvider.prototype.getPaletteEntries = function (this: { elementFactory: unknown } & Record<string, unknown>) {
|
|
||||||
const { create, elementFactory, translate, globalConnect } = this as Record<string, any>;
|
|
||||||
const action = (type: string, label: string, className: string) => ({
|
|
||||||
group: "flow",
|
|
||||||
className,
|
|
||||||
title: translate(label),
|
|
||||||
action: {
|
|
||||||
dragstart: (evt: unknown) => create.start(evt, elementFactory.createShape({ type })),
|
|
||||||
click: (evt: unknown) => create.start(evt, elementFactory.createShape({ type })),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
"lc-connect": {
|
|
||||||
group: "tools",
|
|
||||||
className: "bpmn-icon-connection-multi",
|
|
||||||
title: translate("连线"),
|
|
||||||
action: { click: (evt: unknown) => globalConnect.toggle(evt) },
|
|
||||||
},
|
|
||||||
"lc-sep": { group: "tools", separator: true },
|
|
||||||
"lc-start": action("bpmn:StartEvent", "创建开始", "bpmn-icon-start-event-none"),
|
|
||||||
"lc-end": action("bpmn:EndEvent", "创建结束", "bpmn-icon-end-event-none"),
|
|
||||||
"lc-task": action("bpmn:UserTask", "创建审批节点", "bpmn-icon-user-task"),
|
|
||||||
"lc-notify": action("bpmn:ServiceTask", "创建抄送节点", "bpmn-icon-send-task"),
|
|
||||||
"lc-gateway": action("bpmn:ParallelGateway", "创建并行分支", "bpmn-icon-gateway-parallel"),
|
|
||||||
"lc-condition": action("bpmn:ExclusiveGateway", "创建条件分支", "bpmn-icon-gateway-xor"),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return { __init__: ["paletteProvider"], paletteProvider: ["type", CustomPaletteProvider] };
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 创建设计器实例(调色板收敛 + 汉化;快捷键绑定 window:Ctrl+Z 撤销等) */
|
|
||||||
export function createDesigner(container: HTMLElement): Modeler {
|
|
||||||
return new Modeler({
|
|
||||||
container,
|
|
||||||
keyboard: { bindTo: window },
|
|
||||||
additionalModules: [createPaletteModule(), createTranslateModule()],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
/**
|
|
||||||
* contentJson(+layout) ⇄ bpmn-js 内存图 双向转换(画布模式数据通道)。
|
|
||||||
*
|
|
||||||
* 设计决策(方案 §二):BPMN XML 不作为持久化格式——contentJson 仍是唯一运行时格式,
|
|
||||||
* bpmn-js 仅作内存画布。节点映射:START→StartEvent / END→EndEvent / APPROVAL→UserTask /
|
|
||||||
* PARALLEL_SPLIT|JOIN→ParallelGateway,用扩展属性 flowNodeType 显式区分 split/join
|
|
||||||
* (不靠出入度推断,用户编辑连线后语义不漂移);出边 name 即多出口按钮文案。
|
|
||||||
* 坐标缺失(旧定义无 layout)时用共享 flowLayout 算初始坐标。
|
|
||||||
*/
|
|
||||||
import { layout as autoLayout } from "@/utils/flowLayout";
|
|
||||||
|
|
||||||
/** bpmn 画布尺寸(与 flowLayout 的槽位框不同——bpmn 内建元素有自己尺寸,就不指定 shape 尺寸) */
|
|
||||||
const CANVAS_GAP_X = 96;
|
|
||||||
const CANVAS_ROW_H = 96;
|
|
||||||
const CANVAS_START = { x: 160, y: 160 };
|
|
||||||
|
|
||||||
/** 画布模式读写接口(与后端 contentJson 同构,另含可选 layout 坐标) */
|
|
||||||
export interface CanvasJson {
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
start: string;
|
|
||||||
nodes: Array<Record<string, unknown>>;
|
|
||||||
edges: Array<Record<string, unknown>>;
|
|
||||||
settings: Record<string, unknown>;
|
|
||||||
layout?: Record<string, { x: number; y: number }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BPMN_TYPE: Record<string, string> = {
|
|
||||||
START: "bpmn:StartEvent",
|
|
||||||
END: "bpmn:EndEvent",
|
|
||||||
APPROVAL: "bpmn:UserTask",
|
|
||||||
NOTIFY: "bpmn:ServiceTask",
|
|
||||||
PARALLEL_SPLIT: "bpmn:ParallelGateway",
|
|
||||||
PARALLEL_JOIN: "bpmn:ParallelGateway",
|
|
||||||
CONDITION: "bpmn:ExclusiveGateway",
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 把 contentJson 画到画布上(缺 layout 时自动布局) */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function toBpmn(modeler: any, def: CanvasJson): void {
|
|
||||||
const nodes = def.nodes || [];
|
|
||||||
const edges = def.edges || [];
|
|
||||||
// 自动布局:有保存坐标用保存坐标,否则 Kahn 分层换算为 bpmn 网格
|
|
||||||
let pos: Record<string, { x: number; y: number }> = def.layout || (undefined as never);
|
|
||||||
if (!pos || !Object.keys(pos).length) {
|
|
||||||
const auto = autoLayout({ nodes, edges });
|
|
||||||
pos = {};
|
|
||||||
Object.entries(auto).forEach(([id, p]) => {
|
|
||||||
pos![id] = { x: CANVAS_START.x + p.x + p.w / 2, y: CANVAS_START.y + p.y + p.h / 2 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const modeling = modeler.get("modeling");
|
|
||||||
const canvas = modeler.get("canvas");
|
|
||||||
const root = canvas.getRootElement();
|
|
||||||
const byId: Record<string, unknown> = {};
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
const type = BPMN_TYPE[String(n.type)];
|
|
||||||
if (!type) return; // 未知类型不渲染(local validate 会提示)
|
|
||||||
const p = pos[String(n.id)] || CANVAS_START;
|
|
||||||
const shape = modeling.createShape({ type }, { x: p.x, y: p.y }, root);
|
|
||||||
// id 必须显式落 businessObject:配置回填与跨会话稳定绑定都按 contentJson 的节点 id 走
|
|
||||||
// (若不设,bpmn-js 生成 Task_xxx 随机 id,二次载入时审批人/会签等配置将失配丢失)
|
|
||||||
// 节点名(直编可改);split/join 语义由「载入显式类型 / 读出按出入度」两段规则维持,存储在 contentJson
|
|
||||||
modeling.updateProperties(shape, { id: String(n.id), name: String(n.name || "") });
|
|
||||||
byId[String(n.id)] = shape;
|
|
||||||
});
|
|
||||||
edges.forEach((e) => {
|
|
||||||
const src = byId[String(e.from)];
|
|
||||||
const tgt = byId[String(e.to)];
|
|
||||||
if (!src || !tgt) return;
|
|
||||||
// connect(source, target) 按规则自动建 SequenceFlow
|
|
||||||
const conn = modeling.connect(src, tgt);
|
|
||||||
if (e.name) {
|
|
||||||
modeling.updateProperties(conn, { name: String(e.name) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** bpmn 内存图 → {nodes,edges,layout}:遍历 elementRegistry 序列化回 contentJson 主体 */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function fromBpmn(modeler: any, base: CanvasJson, code: string, name: string): CanvasJson {
|
|
||||||
const registry = modeler.get("elementRegistry");
|
|
||||||
const all = registry.getAll();
|
|
||||||
const nodes: Array<Record<string, unknown>> = [];
|
|
||||||
const edges: Array<Record<string, unknown>> = [];
|
|
||||||
const layoutOut: Record<string, { x: number; y: number }> = {};
|
|
||||||
let startId = "";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const nodeTypeOf = (n: any): string | null => {
|
|
||||||
switch (n.type) {
|
|
||||||
case "bpmn:StartEvent": {
|
|
||||||
return "START";
|
|
||||||
}
|
|
||||||
case "bpmn:EndEvent": {
|
|
||||||
return "END";
|
|
||||||
}
|
|
||||||
case "bpmn:UserTask": {
|
|
||||||
return "APPROVAL";
|
|
||||||
}
|
|
||||||
case "bpmn:ServiceTask": {
|
|
||||||
return "NOTIFY";
|
|
||||||
}
|
|
||||||
case "bpmn:ParallelGateway": {
|
|
||||||
// 读出推断:>1 入 = 汇聚;否则分发(存储在 contentJson,故多数载入已在 JSON 有显式类型)
|
|
||||||
return (n.incoming || []).length > 1 ? "PARALLEL_JOIN" : "PARALLEL_SPLIT";
|
|
||||||
}
|
|
||||||
case "bpmn:ExclusiveGateway": {
|
|
||||||
// CONDITION 无 join 对应物(排他网关不用于汇聚):多入口也按条件分支读出
|
|
||||||
return "CONDITION";
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 节点业务配置(审批人等)以 base 内容继承,这里只做几何/连线
|
|
||||||
all.forEach((n: any) => {
|
|
||||||
const type = nodeTypeOf(n);
|
|
||||||
if (!type) return;
|
|
||||||
const biz = n.businessObject || {};
|
|
||||||
const node: Record<string, unknown> = {
|
|
||||||
id: String(biz.id || n.id),
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
if (biz.name) node.name = biz.name;
|
|
||||||
if (type === "APPROVAL" || type === "NOTIFY" || type === "CONDITION") {
|
|
||||||
// 审批人/抄送对象/判断服务等业务属性:从 base 中同 id 继承(编辑连线不影响已有配置)
|
|
||||||
const prev = base.nodes.find((x) => String(x.id) === String(node.id));
|
|
||||||
if (prev) {
|
|
||||||
const keys = type === "APPROVAL"
|
|
||||||
? ["assignee", "countersign", "deadlineHours"]
|
|
||||||
: type === "NOTIFY" ? ["receivers"] : ["routerKey"];
|
|
||||||
keys.forEach((k) => {
|
|
||||||
if (prev[k] !== undefined) node[k] = prev[k];
|
|
||||||
});
|
|
||||||
if (!node.name && prev.name) node.name = prev.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes.push(node);
|
|
||||||
layoutOut[String(node.id)] = { x: Math.round(n.x), y: Math.round(n.y) };
|
|
||||||
if (type === "START") startId = String(node.id);
|
|
||||||
});
|
|
||||||
all.forEach((n: any) => {
|
|
||||||
(n.outgoing || []).forEach((c: any) => {
|
|
||||||
if (c.type !== "bpmn:SequenceFlow") return;
|
|
||||||
const target = c.target;
|
|
||||||
if (!target) return;
|
|
||||||
const edge: Record<string, unknown> = {
|
|
||||||
from: String(n.businessObject.id || n.id),
|
|
||||||
to: String(target.businessObject.id || target.id),
|
|
||||||
};
|
|
||||||
if (c.businessObject?.name) edge.name = c.businessObject.name;
|
|
||||||
edges.push(edge);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (!startId && nodes.length) {
|
|
||||||
throw new Error("画布缺少开始节点");
|
|
||||||
}
|
|
||||||
// 已有定义继承 settings/审批配置由 base 传入
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
name,
|
|
||||||
start: startId,
|
|
||||||
nodes,
|
|
||||||
edges,
|
|
||||||
settings: base?.settings || { rejectPolicy: "END_REJECTED" },
|
|
||||||
layout: layoutOut,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** contentJson → CanvasJson(对既有 struct 直接加 layout) */
|
|
||||||
export function toCanvasJson(contentJson: string | CanvasJson): CanvasJson {
|
|
||||||
if (typeof contentJson === "string") {
|
|
||||||
return JSON.parse(contentJson) as CanvasJson;
|
|
||||||
}
|
|
||||||
return contentJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 查看器 BPMN DI 形状尺寸(事件 36、网关 50、任务两行文本 150x76) */
|
|
||||||
const DI_SIZE: Record<string, { w: number; h: number }> = {
|
|
||||||
START: { w: 36, h: 36 },
|
|
||||||
END: { w: 36, h: 36 },
|
|
||||||
APPROVAL: { w: 150, h: 76 },
|
|
||||||
NOTIFY: { w: 150, h: 76 },
|
|
||||||
PARALLEL_SPLIT: { w: 50, h: 50 },
|
|
||||||
PARALLEL_JOIN: { w: 50, h: 50 },
|
|
||||||
CONDITION: { w: 50, h: 50 },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* contentJson(+layout) → 可导入只读 Viewer 的 BPMN XML(含 DI 布局)。
|
|
||||||
* 画布视图专用:bpmn-js Viewer 没有 modeling 模块,不能走 toBpmn 的程序化布点,
|
|
||||||
* 直接生成带 BPMNShape/BPMNEdge 的 XML 一次导入;连线给正交 waypoints(右中→中线→左中)。
|
|
||||||
*/
|
|
||||||
export function toBpmnXml(def: CanvasJson): string {
|
|
||||||
const nodes: any[] = def.nodes || [];
|
|
||||||
const edges: any[] = def.edges || [];
|
|
||||||
// 元素中心点:layout 覆盖全部节点用 layout(编辑器保存的是中心坐标),否则回退自动布局
|
|
||||||
const saved = def.layout || {};
|
|
||||||
const useSaved = nodes.length > 0 && nodes.every((n) => saved[String(n.id)]);
|
|
||||||
const centers: Record<string, { x: number; y: number }> = {};
|
|
||||||
if (useSaved) {
|
|
||||||
nodes.forEach((n) => { centers[String(n.id)] = saved[String(n.id)]; });
|
|
||||||
} else {
|
|
||||||
const auto = autoLayout({ nodes, edges });
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
const p = auto[String(n.id)];
|
|
||||||
if (p) centers[String(n.id)] = { x: p.x + p.w / 2, y: p.y + p.h / 2 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const esc = (s: unknown): string =>
|
|
||||||
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
||||||
const body = nodes.filter((n) => BPMN_TYPE[String(n.type)] && centers[String(n.id)]);
|
|
||||||
const flows = edges.filter((e) => centers[String(e.from)] && centers[String(e.to)]);
|
|
||||||
|
|
||||||
const elems = body
|
|
||||||
.map((n) => {
|
|
||||||
const tag = BPMN_TYPE[String(n.type)].replace("bpmn:", "");
|
|
||||||
const name = n.name ? ` name="${esc(n.name)}"` : "";
|
|
||||||
return ` <bpmn:${tag} id="${esc(n.id)}"${name} />`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
const flowElems = flows
|
|
||||||
.map((e, i) => {
|
|
||||||
const name = e.name ? ` name="${esc(e.name)}"` : "";
|
|
||||||
return ` <bpmn:sequenceFlow id="flow_${i}"${name} sourceRef="${esc(e.from)}" targetRef="${esc(e.to)}" />`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
const bounds = (id: string): { x: number; y: number; w: number; h: number } => {
|
|
||||||
const c = centers[id];
|
|
||||||
const type = String(nodes.find((n) => String(n.id) === id)?.type || "APPROVAL");
|
|
||||||
const s = DI_SIZE[type] || DI_SIZE.APPROVAL;
|
|
||||||
return { x: Math.round(c.x - s.w / 2), y: Math.round(c.y - s.h / 2), w: s.w, h: s.h };
|
|
||||||
};
|
|
||||||
const shapes = body
|
|
||||||
.map((n) => {
|
|
||||||
const b = bounds(String(n.id));
|
|
||||||
return ` <bpmndi:BPMNShape id="di_${esc(n.id)}" bpmnElement="${esc(n.id)}">
|
|
||||||
<dc:Bounds x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" />
|
|
||||||
</bpmndi:BPMNShape>`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
const edgeDi = flows
|
|
||||||
.map((e, i) => {
|
|
||||||
const fb = bounds(String(e.from));
|
|
||||||
const tb = bounds(String(e.to));
|
|
||||||
const x1 = fb.x + fb.w;
|
|
||||||
const y1 = fb.y + fb.h / 2;
|
|
||||||
const x2 = tb.x;
|
|
||||||
const y2 = tb.y + tb.h / 2;
|
|
||||||
const mid = Math.round((x1 + x2) / 2);
|
|
||||||
const pts = Math.abs(y1 - y2) < 2
|
|
||||||
? [[x1, y1], [x2, y2]]
|
|
||||||
: [[x1, y1], [mid, y1], [mid, y2], [x2, y2]];
|
|
||||||
const wp = pts.map(([x, y]) => ` <di:waypoint x="${Math.round(x)}" y="${Math.round(y)}" />`).join("\n");
|
|
||||||
return ` <bpmndi:BPMNEdge id="di_flow_${i}" bpmnElement="flow_${i}">\n${wp}\n </bpmndi:BPMNEdge>`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
||||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
|
||||||
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
|
||||||
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
|
||||||
targetNamespace="http://bpmn.io/schema/bpmn">
|
|
||||||
<bpmn:process id="lc_process" isExecutable="true">
|
|
||||||
${elems}
|
|
||||||
${flowElems}
|
|
||||||
</bpmn:process>
|
|
||||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
|
||||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="lc_process">
|
|
||||||
${shapes}
|
|
||||||
${edgeDi}
|
|
||||||
</bpmndi:BPMNPlane>
|
|
||||||
</bpmndi:BPMNDiagram>
|
|
||||||
</bpmn:definitions>`;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import { computed, inject, reactive, ref, watch } from "vue";
|
|||||||
import { Message } from "view-ui-plus";
|
import { Message } from "view-ui-plus";
|
||||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||||
import { flowApi } from "@/api/module/flow";
|
import { flowApi } from "@/api/module/flow";
|
||||||
import { ApprovalNode, EDITOR_CTX } from "../model";
|
import { ApprovalNode, EDITOR_CTX, createApproval } from "../model";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
@@ -28,6 +28,7 @@ const form = reactive({
|
|||||||
userIds: [] as number[],
|
userIds: [] as number[],
|
||||||
countersign: "ANY" as "ALL" | "ANY",
|
countersign: "ANY" as "ALL" | "ANY",
|
||||||
deadlineHours: null as number | null,
|
deadlineHours: null as number | null,
|
||||||
|
multiOutlet: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userCheckModal = ref<any>();
|
const userCheckModal = ref<any>();
|
||||||
@@ -47,6 +48,7 @@ function syncFromNode(): void {
|
|||||||
form.userIds = n.assignee?.type === "USERS" ? [...n.assignee.userIds] : [];
|
form.userIds = n.assignee?.type === "USERS" ? [...n.assignee.userIds] : [];
|
||||||
form.countersign = n.countersign;
|
form.countersign = n.countersign;
|
||||||
form.deadlineHours = n.deadlineHours;
|
form.deadlineHours = n.deadlineHours;
|
||||||
|
form.multiOutlet = !!(n.outlets && n.outlets.length >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -119,6 +121,12 @@ function onOk(): void {
|
|||||||
: { type: "DEPT_KEY", deptKey: form.deptKey };
|
: { type: "DEPT_KEY", deptKey: form.deptKey };
|
||||||
n.countersign = form.countersign;
|
n.countersign = form.countersign;
|
||||||
n.deadlineHours = form.deadlineHours && form.deadlineHours > 0 ? form.deadlineHours : null;
|
n.deadlineHours = form.deadlineHours && form.deadlineHours > 0 ? form.deadlineHours : null;
|
||||||
|
// 开启多出口(2026-10-03 P2):默认 同意/退回 两泳道各放一个待配置审批节点;
|
||||||
|
// 关闭不在抽屉做——泳道头部"恢复单出口"负责(要处理泳道内容的去向)
|
||||||
|
if (form.multiOutlet && !(n.outlets && n.outlets.length >= 2)) {
|
||||||
|
n.outlets = ["同意", "退回"];
|
||||||
|
n.branches = [[createApproval()], [createApproval()]];
|
||||||
|
}
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +205,18 @@ function onRemove(): void {
|
|||||||
<InputNumber v-model="form.deadlineHours" :min="1" placeholder="可空" style="width: 100%" />
|
<InputNumber v-model="form.deadlineHours" :min="1" placeholder="可空" style="width: 100%" />
|
||||||
<div class="hint">超过时限后进入催办提醒(取到达时间 + N 小时)</div>
|
<div class="hint">超过时限后进入催办提醒(取到达时间 + N 小时)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="fld">
|
||||||
|
<label>出口路由</label>
|
||||||
|
<div class="mo-row">
|
||||||
|
<i-switch v-model="form.multiOutlet" />
|
||||||
|
<span class="mo-label">多出口审批(办理人按下按钮选路)</span>
|
||||||
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
开启后卡片下出现出口泳道(默认 同意/退回),出口名即办理按钮文案;泳道尾自动汇合到后继节点。
|
||||||
|
关闭请用泳道头部的"恢复单出口"(需处理泳道内容去向)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<div class="dw-foot">
|
<div class="dw-foot">
|
||||||
@@ -249,6 +269,14 @@ function onRemove(): void {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px 0;
|
gap: 4px 0;
|
||||||
}
|
}
|
||||||
|
.mo-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.mo-label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
.dw-foot {
|
.dw-foot {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: -16px;
|
bottom: -16px;
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
// bpmn-js 画布(方案 §C):拖拽建模器,仅暴露业务 5 类元素;contentJson 为唯一持久化格式,
|
|
||||||
// 本组件负责画布初始化、按名路由的出口命名、以及 fromBpmn 序列化。
|
|
||||||
// bpmn.io 水印为协议要求(左下角 "Powered by bpmn.io"),勿删。
|
|
||||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
|
||||||
import { Message } from "view-ui-plus";
|
|
||||||
import { createDesigner } from "../bpmn/custom";
|
|
||||||
import { toBpmn, fromBpmn, CanvasJson } from "../bpmn/serialize";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
/** contentJson 字符串;空 = 新建骨架 */
|
|
||||||
source: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const emit = defineEmits<{ dirty: []; nodeConfig: [any, any]; edgeBusy: [] }>();
|
|
||||||
|
|
||||||
const wrap = ref<HTMLDivElement | null>(null);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let modeler: any = null;
|
|
||||||
|
|
||||||
/** 选中元素(审批节点弹配置抽屉 / 连线弹命名) */
|
|
||||||
const selected = ref<any>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!wrap.value) return;
|
|
||||||
modeler = createDesigner(wrap.value);
|
|
||||||
// 选中级联:parent emit 让外层判断(APPROVAL → 打开配置抽屉;连线 → 预留命名按钮)
|
|
||||||
modeler.on("selection.changed", (_e: unknown) => {
|
|
||||||
const sel = modeler.get("selection").get();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
selected.value = sel.length === 1 ? sel[0] : null;
|
|
||||||
emit("nodeConfig", selected.value, null);
|
|
||||||
});
|
|
||||||
modeler.on("connectivity-rule-violation", (evt: any) => Message.warning(String(evt?.message || "连线规则不允许")));
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => modeler?.destroy());
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
try {
|
|
||||||
if (props.source && props.source.trim().startsWith("{")) {
|
|
||||||
const def = JSON.parse(props.source) as CanvasJson;
|
|
||||||
if (def.nodes?.length) {
|
|
||||||
// 已有定义:先导入最小骨架再程序化布点连线
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
await modeler.importXML(minimalXml());
|
|
||||||
toBpmn(modeler, { ...def, code: def.code || "", name: def.name || "" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 新建:画默认骨架 start → 审批 → end
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
await modeler.importXML(minimalXml());
|
|
||||||
toBpmn(modeler, skeleton());
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(`流程图载入失败:${e?.message || e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 新建骨架(含默认审批节点,审批人可见在外层点开抽屉配置) */
|
|
||||||
function skeleton(): CanvasJson {
|
|
||||||
return {
|
|
||||||
code: "",
|
|
||||||
name: "",
|
|
||||||
start: "start",
|
|
||||||
nodes: [
|
|
||||||
{ id: "start", type: "START" },
|
|
||||||
{ id: "n1", type: "APPROVAL", name: "步骤 1" },
|
|
||||||
{ id: "end", type: "END" },
|
|
||||||
],
|
|
||||||
edges: [
|
|
||||||
{ from: "start", to: "n1" },
|
|
||||||
{ from: "n1", to: "end" },
|
|
||||||
],
|
|
||||||
settings: { rejectPolicy: "END_REJECTED" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 最小合法 BPMN XML(空 process):程序化布点用 */
|
|
||||||
function minimalXml(): string {
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
||||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
|
||||||
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
|
||||||
targetNamespace="http://bpmn.io/schema/bpmn">
|
|
||||||
<bpmn:process id="lc_process" isExecutable="true" />
|
|
||||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
|
||||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="lc_process" />
|
|
||||||
</bpmndi:BPMNDiagram>
|
|
||||||
</bpmn:definitions>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.source,
|
|
||||||
() => {
|
|
||||||
if (modeler) void init();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ==================== 工具栏 ====================
|
|
||||||
|
|
||||||
function zoom(delta: number) {
|
|
||||||
const canvas = modeler.get("canvas");
|
|
||||||
canvas.zoom(canvas.zoom() + delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetZoom() {
|
|
||||||
modeler.get("canvas").zoom("fit-viewport", "auto");
|
|
||||||
}
|
|
||||||
|
|
||||||
function undo() {
|
|
||||||
modeler.get("commandStack").undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
function redo() {
|
|
||||||
modeler.get("commandStack").redo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 选中连线命名(出口按钮文案):弹 Vue 端 prompt(用 window.prompt 保实现简单) */
|
|
||||||
function renameEdge() {
|
|
||||||
const sel = modeler.get("selection").get();
|
|
||||||
if (sel.length !== 1 || sel[0].type !== "bpmn:SequenceFlow") {
|
|
||||||
Message.warning("请先点击选中一条连线");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const conn = sel[0];
|
|
||||||
const cur = conn.businessObject?.name || "";
|
|
||||||
const name = window.prompt("出口名称(即办理按钮文案;单出口可留空,多出口必须命名)", cur);
|
|
||||||
if (name === null) return;
|
|
||||||
modeler.get("modeling").updateProperties(conn, { name: name.trim() });
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportBpmn() {
|
|
||||||
modeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
|
|
||||||
const blob = new Blob([xml], { type: "application/xml" });
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = URL.createObjectURL(blob);
|
|
||||||
a.download = "flow.bpmn";
|
|
||||||
a.click();
|
|
||||||
setTimeout(() => URL.revokeObjectURL(a.href), 3000);
|
|
||||||
Message.success("已导出 BPMN 文件(不参与保存,仅备档)");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
defineExpose({
|
|
||||||
/** 序列化当前画布(外层合并 code/name/settings 与审批配置) */
|
|
||||||
serialize: (base: CanvasJson | null, code: string, name: string): CanvasJson => {
|
|
||||||
return fromBpmn(modeler, base || ({} as CanvasJson), code, name);
|
|
||||||
},
|
|
||||||
/** 按 contentJson 节点 id 删除元素(卡片抽屉删除回调) */
|
|
||||||
removeNode: (nodeId: string) => {
|
|
||||||
const el = modeler.get("elementRegistry").get(nodeId);
|
|
||||||
if (el) modeler.get("modeling").removeElements([el]);
|
|
||||||
},
|
|
||||||
/** 更新节点名(配置抽屉确定后回写画布) */
|
|
||||||
renameNode: (nodeId: string, name: string) => {
|
|
||||||
const el = modeler.get("elementRegistry").get(nodeId);
|
|
||||||
if (el) modeler.get("modeling").updateProperties(el, { name });
|
|
||||||
},
|
|
||||||
commandStack: () => modeler?.get("commandStack"),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
elementRegistry: () => (modeler ? modeler.get("elementRegistry") : null),
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
markClean: () => modeler?.get("commandStack").clear(),
|
|
||||||
selected: () => modeler?.get("selection").get(),
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="bpmn-wrap">
|
|
||||||
<div class="bpmn-toolbar">
|
|
||||||
<Button size="small" @click="undo">撤销</Button>
|
|
||||||
<Button size="small" @click="redo">重做</Button>
|
|
||||||
<span class="sep" />
|
|
||||||
<Button size="small" @click="zoom(-0.1)">−</Button>
|
|
||||||
<Button size="small" @click="resetZoom">复位</Button>
|
|
||||||
<Button size="small" @click="zoom(0.1)">+</Button>
|
|
||||||
<span class="sep" />
|
|
||||||
<Button size="small" @click="renameEdge">出口命名</Button>
|
|
||||||
<Button size="small" @click="exportBpmn">导出 BPMN</Button>
|
|
||||||
<span class="tip">左侧工具栏拖入节点 · 选中审批节点配置 · 选中连线命名出口(多出口必填)</span>
|
|
||||||
</div>
|
|
||||||
<div ref="wrap" class="bpmn-container" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.bpmn-wrap {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
.bpmn-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid #e8eaec;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
.sep {
|
|
||||||
width: 1px;
|
|
||||||
height: 20px;
|
|
||||||
background: #e8eaec;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
.tip {
|
|
||||||
color: #a5abb8;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
.bpmn-container {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 380px;
|
|
||||||
}
|
|
||||||
/* bpmn.io 水印按协议保留 */
|
|
||||||
.bpmn-container :deep(.bjs-powered-by) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -5,6 +5,7 @@ import NodeCard from "./NodeCard.vue";
|
|||||||
import NotifyCard from "./NotifyCard.vue";
|
import NotifyCard from "./NotifyCard.vue";
|
||||||
import ParallelBox from "./ParallelBox.vue";
|
import ParallelBox from "./ParallelBox.vue";
|
||||||
import ConditionBox from "./ConditionBox.vue";
|
import ConditionBox from "./ConditionBox.vue";
|
||||||
|
import OutletLanes from "./OutletLanes.vue";
|
||||||
import PlusButton from "./PlusButton.vue";
|
import PlusButton from "./PlusButton.vue";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -19,9 +20,10 @@ defineProps<{
|
|||||||
<PlusButton :container="container" :index="0" />
|
<PlusButton :container="container" :index="0" />
|
||||||
<template v-for="(item, i) in items" :key="item.uid">
|
<template v-for="(item, i) in items" :key="item.uid">
|
||||||
<NodeCard v-if="item.kind === 'approval'" :node="item" />
|
<NodeCard v-if="item.kind === 'approval'" :node="item" />
|
||||||
<NotifyCard v-else-if="item.kind === 'notify'" :node="item" />
|
<OutletLanes v-if="item.kind === 'approval' && item.outlets && item.outlets.length >= 2" :node="item" />
|
||||||
|
<NotifyCard v-if="item.kind === 'notify'" :node="item" />
|
||||||
<ParallelBox v-else-if="item.kind === 'parallel'" :node="item" />
|
<ParallelBox v-else-if="item.kind === 'parallel'" :node="item" />
|
||||||
<ConditionBox v-else :node="item" />
|
<ConditionBox v-else-if="item.kind === 'condition'" :node="item" />
|
||||||
<PlusButton :container="container" :index="i + 1" />
|
<PlusButton :container="container" :index="i + 1" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// 多出口审批的出口泳道容器(2026-10-03 P2):蓝系区分条件紫。与条件分支容器完全同构——
|
||||||
|
// 无 JOIN 语义,办理人按下哪个出口按钮走哪条泳道,分支尾隐式汇合到容器后继。
|
||||||
|
// 出口名 Input 即 contentJson 里 APPROVAL 命名出边的 name(按钮文案,必填唯一)。
|
||||||
|
import { computed, inject } from "vue";
|
||||||
|
import { BRANCH_LABELS, EDITOR_CTX, MAX_BRANCHES, MIN_BRANCHES, ApprovalNode } from "../model";
|
||||||
|
import NodeList from "./NodeList.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{ node: ApprovalNode }>();
|
||||||
|
|
||||||
|
const ctx = inject(EDITOR_CTX)!;
|
||||||
|
|
||||||
|
const issues = computed(() => ctx.issueOf(props.node.uid));
|
||||||
|
const hasError = computed(() => issues.value.some((i) => i.level === "error"));
|
||||||
|
const canRemoveBranch = computed(() => (props.node.branches?.length || 0) > MIN_BRANCHES);
|
||||||
|
const canAddBranch = computed(() => (props.node.branches?.length || 0) < MAX_BRANCHES);
|
||||||
|
|
||||||
|
/** 出口名就地编辑(重复/空由校验提示;与 branches 下标一一对应) */
|
||||||
|
function renameOutlet(i: number, v: string): void {
|
||||||
|
props.node.outlets![i] = v;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="outlet-lanes" :class="{ error: hasError }">
|
||||||
|
<div class="ol-head">
|
||||||
|
<div class="ol-title" @click="ctx.openNode(node.uid)">
|
||||||
|
出口路由(按下按钮选路)
|
||||||
|
<span class="ol-hint">(出口名即办理按钮文案;点击审批卡片配置审批人)</span>
|
||||||
|
</div>
|
||||||
|
<Poptip
|
||||||
|
confirm
|
||||||
|
title="退出多出口将删除出口②起的全部泳道节点,仅保留出口①泳道?"
|
||||||
|
ok-text="退出多出口"
|
||||||
|
cancel-text="取消"
|
||||||
|
transfer
|
||||||
|
@on-ok="ctx.exitOutlets(node.uid)"
|
||||||
|
>
|
||||||
|
<span class="ol-exit">恢复单出口</span>
|
||||||
|
</Poptip>
|
||||||
|
</div>
|
||||||
|
<div class="ol-cols">
|
||||||
|
<div v-for="(branch, i) in node.branches" :key="i" class="ol-col">
|
||||||
|
<div class="ol-col-head">
|
||||||
|
<span class="ol-col-no">{{ BRANCH_LABELS[i] || i + 1 }}</span>
|
||||||
|
<input
|
||||||
|
class="ol-outlet"
|
||||||
|
:value="node.outlets![i]"
|
||||||
|
placeholder="出口名(按钮文案,必填唯一)"
|
||||||
|
maxlength="32"
|
||||||
|
@input="renameOutlet(i, ($event.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<Poptip
|
||||||
|
v-if="canRemoveBranch"
|
||||||
|
confirm
|
||||||
|
title="删除该出口及其泳道节点?"
|
||||||
|
ok-text="删除"
|
||||||
|
cancel-text="取消"
|
||||||
|
transfer
|
||||||
|
@on-ok="ctx.removeBranch(node.uid, i)"
|
||||||
|
>
|
||||||
|
<span class="ol-col-del" title="删除出口">×</span>
|
||||||
|
</Poptip>
|
||||||
|
</div>
|
||||||
|
<NodeList :items="branch" :container="`${node.uid}:${i}`" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button v-if="canAddBranch" class="ol-add" size="small" long @click="ctx.addBranch(node.uid)">
|
||||||
|
+ 添加出口
|
||||||
|
</Button>
|
||||||
|
<div v-for="(issue, k) in issues" :key="k" class="ol-issue" :class="issue.level">⚠ {{ issue.text }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.outlet-lanes {
|
||||||
|
border: 1.5px dashed #5cadff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
background: rgba(240, 249, 255, 0.45);
|
||||||
|
}
|
||||||
|
.outlet-lanes.error {
|
||||||
|
border-color: #ed4014;
|
||||||
|
}
|
||||||
|
.ol-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.ol-title {
|
||||||
|
text-align: center;
|
||||||
|
color: #2d8cf0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ol-title:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ol-hint {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #82a8d8;
|
||||||
|
}
|
||||||
|
.ol-exit {
|
||||||
|
flex: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
|
.ol-exit:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ol-cols {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.ol-col {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 10px 14px;
|
||||||
|
min-width: 230px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ol-col-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
.ol-col-no {
|
||||||
|
flex: none;
|
||||||
|
color: #2d8cf0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.ol-outlet {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #dfe6ee;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2d8cf0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ol-outlet:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2d8cf0;
|
||||||
|
}
|
||||||
|
.ol-col-del {
|
||||||
|
flex: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #c5c9d1;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.ol-col-del:hover {
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
|
.ol-add {
|
||||||
|
margin-top: 12px;
|
||||||
|
border-style: dashed;
|
||||||
|
color: #2d8cf0;
|
||||||
|
border-color: #2d8cf0;
|
||||||
|
background: transparent;
|
||||||
|
width: auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
.ol-add:hover {
|
||||||
|
background: #f0faff;
|
||||||
|
color: #2b85e4;
|
||||||
|
border-color: #2d8cf0;
|
||||||
|
}
|
||||||
|
.ol-issue {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.ol-issue.error {
|
||||||
|
color: #ed4014;
|
||||||
|
}
|
||||||
|
.ol-issue.warn {
|
||||||
|
color: #ff9900;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -73,7 +73,18 @@ export function toContentJson(model: EditorModel): DefJson {
|
|||||||
if (item.deadlineHours) node.deadlineHours = item.deadlineHours;
|
if (item.deadlineHours) node.deadlineHours = item.deadlineHours;
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
linkTo(id);
|
linkTo(id);
|
||||||
|
if (item.outlets && item.outlets.length >= 2 && item.branches) {
|
||||||
|
// 多出口(2026-10-03 P2):APPROVAL 的多条命名出边即按钮选路,
|
||||||
|
// 分支尾隐式汇合到后继(与条件分支容器完全同构)
|
||||||
|
const tails: string[] = [];
|
||||||
|
item.branches.forEach((branch, bi) => {
|
||||||
|
tails.push(...walk(branch, id, item.outlets![bi]));
|
||||||
|
});
|
||||||
|
cursor = null;
|
||||||
|
pending = tails;
|
||||||
|
} else {
|
||||||
cursor = id;
|
cursor = id;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (item.kind === "notify") {
|
if (item.kind === "notify") {
|
||||||
@@ -323,11 +334,39 @@ export function fromContentJson(def: any): ParsedDef {
|
|||||||
if (!n) throw new Error(`引用了不存在的节点:${cur}`);
|
if (!n) throw new Error(`引用了不存在的节点:${cur}`);
|
||||||
|
|
||||||
if (n.type === "APPROVAL") {
|
if (n.type === "APPROVAL") {
|
||||||
if ((out[cur] || []).length !== 1) {
|
const heads = out[cur] || [];
|
||||||
throw new Error(`审批节点「${n.name || cur}」的出边必须恰好 1 条`);
|
if (heads.length === 1) {
|
||||||
}
|
|
||||||
list.push(toApprovalNode(n));
|
list.push(toApprovalNode(n));
|
||||||
cur = out[cur][0];
|
cur = heads[0];
|
||||||
|
} else if (heads.length >= 2) {
|
||||||
|
// 多出口审批:出口名=出边 name(按钮文案),复用 CONDITION 的汇合点算法
|
||||||
|
const mergeId = findMerge(cur);
|
||||||
|
const branches = heads.map((head) => {
|
||||||
|
const r = walkList(head, mergeId);
|
||||||
|
if (r.endedAt !== mergeId) {
|
||||||
|
throw new Error("多出口审批未汇聚到同一个节点,无法还原为卡片视图");
|
||||||
|
}
|
||||||
|
return r.list;
|
||||||
|
});
|
||||||
|
const outlets = heads.map((head) => {
|
||||||
|
const e = edges.find((x) => x.from === cur && x.to === head);
|
||||||
|
return e && e.name ? String(e.name) : "";
|
||||||
|
});
|
||||||
|
if (outlets.some((o) => !o || !o.trim())) {
|
||||||
|
throw new Error(`多出口审批「${n.name || cur}」的每个出口必须命名(按钮文案)`);
|
||||||
|
}
|
||||||
|
const dup = outlets.find((o, i) => outlets.indexOf(o) !== i);
|
||||||
|
if (dup) {
|
||||||
|
throw new Error(`多出口审批出口名重复:「${dup}」(按钮文案必须唯一)`);
|
||||||
|
}
|
||||||
|
const node = toApprovalNode(n);
|
||||||
|
node.outlets = outlets;
|
||||||
|
node.branches = branches.map((b) => (b.length ? b : [createApproval()]));
|
||||||
|
list.push(node);
|
||||||
|
cur = mergeId;
|
||||||
|
} else {
|
||||||
|
throw new Error(`审批节点「${n.name || cur}」出边数非法`);
|
||||||
|
}
|
||||||
} else if (n.type === "NOTIFY") {
|
} else if (n.type === "NOTIFY") {
|
||||||
if ((out[cur] || []).length !== 1) {
|
if ((out[cur] || []).length !== 1) {
|
||||||
throw new Error(`抄送节点「${n.name || cur}」的出边必须恰好 1 条`);
|
throw new Error(`抄送节点「${n.name || cur}」的出边必须恰好 1 条`);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 流程编辑器(方案 §3):钉钉式卡片流——纵向节点卡片 + 节点间"+"插入 + 点卡片右侧抽屉配置,免连线。
|
// 流程编辑器(飞书流派,2026-10-03 画布退役):卡片树是唯一编辑形态与数据源;
|
||||||
|
// 多出口=审批节点出口按钮+泳道(OutletLanes),结构域封死 → 出图确定性布局。
|
||||||
//
|
//
|
||||||
// 入口三态(query):无参=新建;?id={defId}=编辑草稿;?id={defId}&baseGuid={flowGuid}=升版本(载入该版本内容为底稿)。
|
// 入口三态(query):无参=新建;?id={defId}=编辑草稿;?id={defId}&baseGuid={flowGuid}=升版本(载入该版本内容为底稿)。
|
||||||
// 保存只产草稿,走既有 POST /api/flow/admin/def/save;发布仍留在流程定义列表页(后端 DefValidator 终审)。
|
// 保存只产草稿,走既有 POST /api/flow/admin/def/save;发布仍留在流程定义列表页(后端 DefValidator 终审)。
|
||||||
@@ -25,6 +26,7 @@ import {
|
|||||||
createEmptyModel,
|
createEmptyModel,
|
||||||
createNotify,
|
createNotify,
|
||||||
createParallel,
|
createParallel,
|
||||||
|
branchesOf,
|
||||||
findItem,
|
findItem,
|
||||||
removeItem,
|
removeItem,
|
||||||
resolveContainer,
|
resolveContainer,
|
||||||
@@ -38,7 +40,6 @@ import NotifyDrawer from "./components/NotifyDrawer.vue";
|
|||||||
import ConditionDrawer from "./components/ConditionDrawer.vue";
|
import ConditionDrawer from "./components/ConditionDrawer.vue";
|
||||||
import SettingsDrawer from "./components/SettingsDrawer.vue";
|
import SettingsDrawer from "./components/SettingsDrawer.vue";
|
||||||
import JsonDrawer from "./components/JsonDrawer.vue";
|
import JsonDrawer from "./components/JsonDrawer.vue";
|
||||||
import BpmnCanvas from "./components/BpmnCanvas.vue";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,174 +64,9 @@ const userNames = reactive<Record<number, string>>({});
|
|||||||
/** 条件分支的判断服务注册(flw_router;拉取失败为空数组,校验降级提示) */
|
/** 条件分支的判断服务注册(flw_router;拉取失败为空数组,校验降级提示) */
|
||||||
const routers = reactive<RouterOption[]>([]);
|
const routers = reactive<RouterOption[]>([]);
|
||||||
|
|
||||||
// ==================== 双模式统一(2026-09-17):卡片树 = 唯一数据源,画布 = 其可视化投影 ====================
|
|
||||||
/**
|
|
||||||
* 旧实现画布/卡片各持一份基准(sourceJson vs model),切换后互相读不到对方改动,"切回去数据变初始化"。
|
|
||||||
* 现统一为:EditorModel 永远持有业务配置(审批人/会签/时限)与卡片结构;
|
|
||||||
* - 进画布:canvasSource = toContentJson(model),节点 id 直接用卡片 uid(convert.ts),天然一一对应;
|
|
||||||
* - 出画布(切卡片/保存/源码应用后):serializeUnified 以 model 为 base 让 bpmn 画布结构性改动
|
|
||||||
* (新增节点/改连线/布局)叠加其上,再经 fromContentJson 反解回卡片树;卡片树不支持的结构(多出口
|
|
||||||
* DAG 等)拒绝切卡片,留在画布编辑。
|
|
||||||
*/
|
|
||||||
/** 编辑形态:canvas=bpmn-js 画布(支持多出口/跨段连线),cards=卡片流 */
|
|
||||||
const editMode = ref<"canvas" | "cards">("canvas");
|
|
||||||
/** 画布侧数据是否可反解为卡片树(载入时探测,决定卡片单选是否可用) */
|
|
||||||
const cardsBroken = ref(false);
|
|
||||||
const bpmnWrap = ref<InstanceType<typeof BpmnCanvas> | null>(null);
|
|
||||||
/** 画布数据源:载入用原始 contentJson;由卡片切回画布时用卡片树投影覆盖 */
|
|
||||||
const canvasSource = ref("");
|
|
||||||
/** 载入时的原始 contentJson(画布上"卡片树不存在"的新画节点配置兜底) */
|
|
||||||
const canvasBase = ref<any>(null);
|
|
||||||
/** 画布上新画节点的配置快照(element id → ApprovalNode 配置),保存时并入 nodes */
|
|
||||||
const canvasConfigs = reactive<Record<string, any>>({});
|
|
||||||
/** 原始 contentJson 快照(画布裁剪兜底与 JsonDrawer 应用基准) */
|
|
||||||
const sourceJson = ref("");
|
|
||||||
|
|
||||||
function onCanvasNode(_el: any): void {
|
|
||||||
// selection.changed 自画布 emit:选中审批/抄送/条件分支节点 → 打开对应配置抽屉
|
|
||||||
const isApproval = _el && _el.type === "bpmn:UserTask";
|
|
||||||
const isNotify = _el && _el.type === "bpmn:ServiceTask";
|
|
||||||
const isCondition = _el && _el.type === "bpmn:ExclusiveGateway";
|
|
||||||
if (!isApproval && !isNotify && !isCondition) return;
|
|
||||||
const id = String(_el.businessObject.id || _el.id);
|
|
||||||
// 条件分支的画布元素 id 是 JSON id(uid+"c" 后缀),卡片树按 uid 查(uid 由本编辑器生成,不带 c 尾)
|
|
||||||
const lookupId = isCondition && id.endsWith("c") ? id.slice(0, -1) : id;
|
|
||||||
const item = findItem(model, lookupId);
|
|
||||||
if (item && (item.kind === "approval" || item.kind === "notify" || item.kind === "condition")) {
|
|
||||||
// 卡片树已有节点:抽屉直接编辑卡片本体(id=uid 天然对应)
|
|
||||||
openNode(item.uid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 画布上新画的节点(卡片树里没有):配置进 canvasConfigs,保存时并入
|
|
||||||
const kind = isNotify ? "notify" : isCondition ? "condition" : "approval";
|
|
||||||
const baseNode = (canvasBase.value?.nodes || []).find((n: any) => n.id === id) || {};
|
|
||||||
if (!canvasConfigs[id]) {
|
|
||||||
canvasConfigs[id] =
|
|
||||||
kind === "notify"
|
|
||||||
? {
|
|
||||||
uid: id,
|
|
||||||
kind: "notify",
|
|
||||||
name: _el.businessObject.name || baseNode.name || "抄送",
|
|
||||||
receivers: baseNode.receivers || { type: "USERS", userIds: [] },
|
|
||||||
}
|
|
||||||
: kind === "condition"
|
|
||||||
? {
|
|
||||||
uid: id,
|
|
||||||
kind: "condition",
|
|
||||||
name: _el.businessObject.name || baseNode.name || "条件判断",
|
|
||||||
routerKey: baseNode.routerKey || "",
|
|
||||||
outlets: [],
|
|
||||||
branches: [],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
uid: id,
|
|
||||||
kind: "approval",
|
|
||||||
name: _el.businessObject.name || baseNode.name || "审批",
|
|
||||||
assignee: baseNode.assignee || null,
|
|
||||||
countersign: baseNode.countersign || "ANY",
|
|
||||||
deadlineHours: baseNode.deadlineHours || null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
editingUid.value = id;
|
|
||||||
if (canvasConfigs[id].kind === "notify") {
|
|
||||||
notifyDrawer.value = true;
|
|
||||||
} else if (canvasConfigs[id].kind === "condition") {
|
|
||||||
conditionDrawer.value = true;
|
|
||||||
} else {
|
|
||||||
nodeDrawer.value = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function canvasRemoveNode(uid: string): void {
|
|
||||||
bpmnWrap.value?.removeNode(uid);
|
|
||||||
delete canvasConfigs[uid];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 画布序列化 → contentJson 字符串:结构性内容(节点/连线/布局)出自画布,
|
|
||||||
* 业务配置(审批人/会签/时限/抄送对象)以卡片树为 base 按 id 继承,settings(含催办)随 base 带出 */
|
|
||||||
function serializeUnified(): string {
|
|
||||||
const canvasJson = snapshotCanvasObject();
|
|
||||||
// canvasConfigs(画布新画节点的抽屉配置)按 id 并入
|
|
||||||
(canvasJson.nodes || []).forEach((n: any) => {
|
|
||||||
const cfg = canvasConfigs[String(n.id)];
|
|
||||||
if (!cfg) return;
|
|
||||||
if (cfg.kind === "notify" && n.type === "NOTIFY") {
|
|
||||||
n.name = cfg.name || n.name;
|
|
||||||
if (cfg.receivers) n.receivers = cfg.receivers;
|
|
||||||
} else if (cfg.kind === "approval" && n.type === "APPROVAL") {
|
|
||||||
n.name = cfg.name || n.name;
|
|
||||||
if (cfg.assignee) n.assignee = cfg.assignee;
|
|
||||||
if (cfg.countersign) n.countersign = cfg.countersign;
|
|
||||||
if (cfg.deadlineHours) n.deadlineHours = cfg.deadlineHours;
|
|
||||||
} else if (cfg.kind === "condition" && n.type === "CONDITION") {
|
|
||||||
n.name = cfg.name || n.name;
|
|
||||||
if (cfg.routerKey) n.routerKey = cfg.routerKey;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return JSON.stringify(canvasJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
const issues = computed<Issue[]>(() => validateModel(model, depts, routers));
|
||||||
const defJson = computed(() => toContentJson(model));
|
const defJson = computed(() => toContentJson(model));
|
||||||
|
|
||||||
/** 序列化 base:原始 contentJson 为底(保住卡片树不支持结构/新画节点的既有业务配置),
|
|
||||||
* code/name/settings 与卡片树审批/抄送节点配置(审批人/会签/时限/抄送对象)以卡片树为准覆盖 */
|
|
||||||
function buildSerializeBase(): any {
|
|
||||||
const modelDef = toContentJson(model);
|
|
||||||
const base = JSON.parse(JSON.stringify(canvasBase.value || {}));
|
|
||||||
base.code = model.code.trim();
|
|
||||||
base.name = model.name.trim();
|
|
||||||
base.settings = JSON.parse(JSON.stringify(modelDef.settings));
|
|
||||||
const byId: Record<string, any> = {};
|
|
||||||
(base.nodes || []).forEach((n: any) => {
|
|
||||||
byId[String(n.id)] = n;
|
|
||||||
});
|
|
||||||
// 全层级遍历(含并行分支内节点):只覆盖 id 能对上的,画布独有节点不受影响
|
|
||||||
walkItems(model.list, (item) => {
|
|
||||||
if (item.kind === "approval") {
|
|
||||||
const merged: any = { ...(byId[item.uid] || {}) };
|
|
||||||
merged.id = item.uid;
|
|
||||||
merged.type = "APPROVAL";
|
|
||||||
merged.name = item.name || "审批";
|
|
||||||
if (item.assignee) merged.assignee = JSON.parse(JSON.stringify(item.assignee));
|
|
||||||
if (item.countersign === "ANY") merged.countersign = "ANY";
|
|
||||||
else delete merged.countersign; // 后端缺省即会签,不回写
|
|
||||||
if (item.deadlineHours) merged.deadlineHours = item.deadlineHours;
|
|
||||||
byId[item.uid] = merged;
|
|
||||||
} else if (item.kind === "notify") {
|
|
||||||
const merged: any = { ...(byId[item.uid] || {}) };
|
|
||||||
merged.id = item.uid;
|
|
||||||
merged.type = "NOTIFY";
|
|
||||||
merged.name = item.name || "抄送";
|
|
||||||
if (item.receivers) merged.receivers = JSON.parse(JSON.stringify(item.receivers));
|
|
||||||
byId[item.uid] = merged;
|
|
||||||
} else if (item.kind === "condition") {
|
|
||||||
// 条件分支的 JSON id 带 c 后缀(convert.ts 生成约定),routerKey/名称随卡片树覆盖
|
|
||||||
const jsonId = `${item.uid}c`;
|
|
||||||
const merged: any = { ...(byId[jsonId] || {}) };
|
|
||||||
merged.id = jsonId;
|
|
||||||
merged.type = "CONDITION";
|
|
||||||
merged.name = item.name || "条件判断";
|
|
||||||
merged.routerKey = item.routerKey || "";
|
|
||||||
byId[jsonId] = merged;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
base.nodes = Object.values(byId);
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 画布未挂载(载入中/刚切换未挂好)时,快照退回卡片树投影 */
|
|
||||||
function snapshotCanvasObject(): any {
|
|
||||||
if (!bpmnWrap.value) return JSON.parse(JSON.stringify(defJson.value));
|
|
||||||
const canvasJson = bpmnWrap.value?.serialize(
|
|
||||||
buildSerializeBase(),
|
|
||||||
model.code.trim(),
|
|
||||||
model.name.trim()
|
|
||||||
);
|
|
||||||
if (!canvasJson) throw new Error("画布不可用");
|
|
||||||
return canvasJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
/** 归一化(去 layout 坐标、节点按 id 排序):卡片树投影与画布序列化表示不同但内容等价,脏比较必须同表示 */
|
||||||
function canonicalJson(content: any): string {
|
function canonicalJson(content: any): string {
|
||||||
const copy: any = JSON.parse(JSON.stringify(content));
|
const copy: any = JSON.parse(JSON.stringify(content));
|
||||||
@@ -249,13 +85,9 @@ function canonicalJson(content: any): string {
|
|||||||
return JSON.stringify(copy);
|
return JSON.stringify(copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 脏检查基准(载入/保存后刷新);随编辑形态取同一内容的对应投影,切换模式后不因表示差异误报 */
|
/** 脏检查基准(载入/保存后刷新) */
|
||||||
const savedSnapshot = ref("");
|
const savedSnapshot = ref("");
|
||||||
const currentSnapshot = computed(() =>
|
const currentSnapshot = computed(() => canonicalJson(JSON.parse(JSON.stringify(defJson.value))));
|
||||||
editMode.value === "cards"
|
|
||||||
? canonicalJson(JSON.parse(JSON.stringify(defJson.value)))
|
|
||||||
: canonicalJson(snapshotCanvasObject())
|
|
||||||
);
|
|
||||||
const isDirty = computed(() => !loading.value && currentSnapshot.value !== savedSnapshot.value);
|
const isDirty = computed(() => !loading.value && currentSnapshot.value !== savedSnapshot.value);
|
||||||
|
|
||||||
const nodeDrawer = ref(false);
|
const nodeDrawer = ref(false);
|
||||||
@@ -266,34 +98,16 @@ const jsonDrawer = ref(false);
|
|||||||
const editingUid = ref("");
|
const editingUid = ref("");
|
||||||
|
|
||||||
const editingNode = computed<ApprovalNode | null>(() => {
|
const editingNode = computed<ApprovalNode | null>(() => {
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
|
||||||
if (item && item.kind === "approval") return item;
|
|
||||||
const cfg = canvasConfigs[editingUid.value];
|
|
||||||
return cfg && cfg.kind === "approval" ? cfg : null;
|
|
||||||
}
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||||
return item && item.kind === "approval" ? item : null;
|
return item && item.kind === "approval" ? item : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const editingNotify = computed<NotifyNode | null>(() => {
|
const editingNotify = computed<NotifyNode | null>(() => {
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
|
||||||
if (item && item.kind === "notify") return item;
|
|
||||||
const cfg = canvasConfigs[editingUid.value];
|
|
||||||
return cfg && cfg.kind === "notify" ? cfg : null;
|
|
||||||
}
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||||
return item && item.kind === "notify" ? item : null;
|
return item && item.kind === "notify" ? item : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const editingCondition = computed<ConditionNode | null>(() => {
|
const editingCondition = computed<ConditionNode | null>(() => {
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
|
||||||
if (item && item.kind === "condition") return item;
|
|
||||||
const cfg = canvasConfigs[editingUid.value];
|
|
||||||
return cfg && cfg.kind === "condition" ? cfg : null;
|
|
||||||
}
|
|
||||||
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
const item = editingUid.value ? findItem(model, editingUid.value) : null;
|
||||||
return item && item.kind === "condition" ? item : null;
|
return item && item.kind === "condition" ? item : null;
|
||||||
});
|
});
|
||||||
@@ -366,8 +180,7 @@ async function load(): Promise<void> {
|
|||||||
if (queryId && !queryGuid) editId.value = queryId;
|
if (queryId && !queryGuid) editId.value = queryId;
|
||||||
|
|
||||||
if (!queryId) {
|
if (!queryId) {
|
||||||
// 新建:画布源取卡片树投影(默认模板的审批节点 uid 与卡片一一对应),不用通用骨架
|
// 新建:卡片树默认模板起步
|
||||||
canvasSource.value = JSON.stringify(defJson.value);
|
|
||||||
savedSnapshot.value = currentSnapshot.value;
|
savedSnapshot.value = currentSnapshot.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -377,18 +190,17 @@ async function load(): Promise<void> {
|
|||||||
sourceVersion.value = d?.version ?? 0;
|
sourceVersion.value = d?.version ?? 0;
|
||||||
model.code = d?.code || "";
|
model.code = d?.code || "";
|
||||||
model.name = d?.name || "";
|
model.name = d?.name || "";
|
||||||
sourceJson.value = d?.contentJson || "{}";
|
|
||||||
canvasBase.value = safeJson(sourceJson.value);
|
|
||||||
canvasSource.value = sourceJson.value;
|
|
||||||
let parsedOk = true;
|
|
||||||
try {
|
try {
|
||||||
applyParsed(fromContentJson(JSON.parse(d?.contentJson || "{}")));
|
applyParsed(fromContentJson(JSON.parse(d?.contentJson || "{}")));
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// 卡片树反解失败(多出口等结构):留在画布模式,卡片单选禁用
|
Modal.error({
|
||||||
parsedOk = false;
|
title: "该定义无法用卡片编辑器打开",
|
||||||
void e;
|
content: `内容包含卡片视图不支持的结构(${e?.message || e})。此类结构多为历史画布草稿,请从流程定义列表处理。`,
|
||||||
|
onOk: () => router.replace("/admin/defs"),
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
cardsBroken.value = !parsedOk;
|
|
||||||
await resolveUserNames();
|
await resolveUserNames();
|
||||||
savedSnapshot.value = currentSnapshot.value;
|
savedSnapshot.value = currentSnapshot.value;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -396,66 +208,6 @@ async function load(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeJson(s: string): any {
|
|
||||||
try {
|
|
||||||
return JSON.parse(s || "{}");
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 模式切换(双向互通) ====================
|
|
||||||
|
|
||||||
/** 卡片 → 画布:画布源换成卡片树投影(画布重挂载即拿到卡片最新结构/配置) */
|
|
||||||
function toCanvas(): void {
|
|
||||||
canvasSource.value = JSON.stringify(defJson.value);
|
|
||||||
editMode.value = "canvas";
|
|
||||||
bumpSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 画布 → 卡片:画布结构经 fromContentJson 反解回卡片树;不支持的结构拒绝切换 */
|
|
||||||
function toCards(): void {
|
|
||||||
let content = "";
|
|
||||||
try {
|
|
||||||
content = serializeUnified();
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(`画布数据读取失败:${e?.message || e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!content) {
|
|
||||||
Message.warning("画布为空,无法切换到卡片视图");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
applyParsed(fromContentJson(JSON.parse(content)));
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(`画布含卡片视图不支持的结构(${e?.message || e}),请继续用画布编辑或先简化结构`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.keys(canvasConfigs).forEach((k) => delete canvasConfigs[k]);
|
|
||||||
editMode.value = "cards";
|
|
||||||
bumpSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 切换后按"切换前是否已脏"重置脏基准:干净切换 representations 等价,直接对齐;脏切换保留脏态 */
|
|
||||||
function bumpSnapshot(): void {
|
|
||||||
savedSnapshot.value = currentSnapshot.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onModeSwitch(next: string | number | boolean): void {
|
|
||||||
if (loading.value) return;
|
|
||||||
if (next === editMode.value) return;
|
|
||||||
if (next === "cards" && cardsBroken.value) {
|
|
||||||
Message.warning("该定义包含卡片视图不支持的结构(如多出口/退回环路),请用画布编辑");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (next === "canvas") {
|
|
||||||
toCanvas();
|
|
||||||
} else {
|
|
||||||
toCards();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 编辑操作(画布子树经 ctx 共享) ====================
|
// ==================== 编辑操作(画布子树经 ctx 共享) ====================
|
||||||
|
|
||||||
/** 定位某节点所在的兄弟列表(删除前的"至少保留一个"判断用) */
|
/** 定位某节点所在的兄弟列表(删除前的"至少保留一个"判断用) */
|
||||||
@@ -485,12 +237,6 @@ function openNode(uid: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeNode(uid: string): void {
|
function removeNode(uid: string): void {
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
// 卡片树是唯一数据源:卡片已有节点从两侧一起删;画布新画节点只删画布+配置快照
|
|
||||||
if (findItem(model, uid)) removeItem(model, uid);
|
|
||||||
canvasRemoveNode(uid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const siblings = containerOfItem(uid);
|
const siblings = containerOfItem(uid);
|
||||||
if (siblings && siblings.length <= 1) {
|
if (siblings && siblings.length <= 1) {
|
||||||
Message.warning("每个流程/分支至少保留一个节点");
|
Message.warning("每个流程/分支至少保留一个节点");
|
||||||
@@ -521,7 +267,7 @@ function insertItem(container: string, index: number, kind: "approval" | "parall
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 添加分支(并行/条件分支容器通用;条件分支同步补一个占位出口名) */
|
/** 添加分支(并行/条件分支/多出口审批容器通用;条件分支与出口同步补一个占位出口名) */
|
||||||
function addBranch(containerUid: string): void {
|
function addBranch(containerUid: string): void {
|
||||||
const item = findItem(model, containerUid);
|
const item = findItem(model, containerUid);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
@@ -531,18 +277,42 @@ function addBranch(containerUid: string): void {
|
|||||||
const no = item.branches.length + 1;
|
const no = item.branches.length + 1;
|
||||||
item.branches.push([createApproval()]);
|
item.branches.push([createApproval()]);
|
||||||
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
||||||
|
} else if (item.kind === "approval" && item.outlets && item.outlets.length >= 2) {
|
||||||
|
const no = (item.branches?.length || 0) + 1;
|
||||||
|
(item.branches = item.branches || []).push([createApproval()]);
|
||||||
|
item.outlets.push(`出口${["一", "二", "三", "四", "五", "六"][no - 1] || no}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeBranch(containerUid: string, index: number): void {
|
function removeBranch(containerUid: string, index: number): void {
|
||||||
const item = findItem(model, containerUid);
|
const item = findItem(model, containerUid);
|
||||||
if (!item || (item.kind !== "parallel" && item.kind !== "condition")) return;
|
const isOutlet = item.kind === "approval" && !!(item.outlets && item.outlets.length >= 2);
|
||||||
if (item.branches.length <= 2) {
|
if (!item || (item.kind !== "parallel" && item.kind !== "condition" && !isOutlet)) return;
|
||||||
|
const branches = item.branches!;
|
||||||
|
if (branches.length <= 2) {
|
||||||
Message.warning("分支至少保留 2 个");
|
Message.warning("分支至少保留 2 个");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
item.branches.splice(index, 1);
|
branches.splice(index, 1);
|
||||||
if (item.kind === "condition") item.outlets.splice(index, 1);
|
if (item.kind === "condition" || isOutlet) item.outlets!.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 退出多出口:仅保留出口①泳道内容,替换为「本节点(去 outlets) + ①泳道节点序列」 */
|
||||||
|
function exitOutlets(uid: string): void {
|
||||||
|
const rec = (items: EditorItem[]): boolean => {
|
||||||
|
const i = items.findIndex((it) => it.uid === uid);
|
||||||
|
if (i >= 0) {
|
||||||
|
const node = items[i] as ApprovalNode;
|
||||||
|
const lane0 = node.branches?.[0] || [];
|
||||||
|
const clean: ApprovalNode = { ...node };
|
||||||
|
delete clean.outlets;
|
||||||
|
delete clean.branches;
|
||||||
|
items.splice(i, 1, clean, ...lane0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return items.some((it) => branchesOf(it)?.some((b) => rec(b)) || false);
|
||||||
|
};
|
||||||
|
rec(model.list);
|
||||||
}
|
}
|
||||||
|
|
||||||
provide<EditorCtx>(EDITOR_CTX, {
|
provide<EditorCtx>(EDITOR_CTX, {
|
||||||
@@ -556,6 +326,7 @@ provide<EditorCtx>(EDITOR_CTX, {
|
|||||||
insertItem,
|
insertItem,
|
||||||
addBranch,
|
addBranch,
|
||||||
removeBranch,
|
removeBranch,
|
||||||
|
exitOutlets,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== 保存 / 离开 ====================
|
// ==================== 保存 / 离开 ====================
|
||||||
@@ -564,30 +335,9 @@ function onUrgeSave(urge: UrgeSetting): void {
|
|||||||
model.settings.urge = urge;
|
model.settings.urge = urge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 抽屉关闭(确定/删除)后,画布上的节点名回写(配置改的是卡片树本体) */
|
/** 源码抽屉应用:写回卡片树(应用源码后 isDirty 自然为 true,预期行为) */
|
||||||
watch([nodeDrawer, notifyDrawer, conditionDrawer], ([aOpen, nOpen, cOpen], [aWas, nWas, cWas]) => {
|
|
||||||
const closed = (open: boolean, was: boolean) => !open && was;
|
|
||||||
if (!closed(aOpen, aWas) && !closed(nOpen, nWas) && !closed(cOpen, cWas)) return;
|
|
||||||
if (editMode.value !== "canvas" || !bpmnWrap.value) return;
|
|
||||||
const item = findItem(model, editingUid.value);
|
|
||||||
if (item && item.kind === "approval") {
|
|
||||||
bpmnWrap.value.renameNode(item.uid, item.name);
|
|
||||||
}
|
|
||||||
if (item && item.kind === "notify") {
|
|
||||||
bpmnWrap.value.renameNode(item.uid, item.name);
|
|
||||||
}
|
|
||||||
if (item && item.kind === "condition") {
|
|
||||||
bpmnWrap.value.renameNode(`${item.uid}c`, item.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 源码抽屉应用:写回卡片树;画布模式下画布源同步换投影,避免看到旧结构 */
|
|
||||||
function onJsonApplied(p: ParsedDef): void {
|
function onJsonApplied(p: ParsedDef): void {
|
||||||
applyParsed(p);
|
applyParsed(p);
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
canvasSource.value = JSON.stringify(defJson.value);
|
|
||||||
bumpSnapshot();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave(): Promise<void> {
|
async function onSave(): Promise<void> {
|
||||||
@@ -600,36 +350,11 @@ async function onSave(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const errors = countErrors(issues.value);
|
const errors = countErrors(issues.value);
|
||||||
if (errors.length && editMode.value === "cards") {
|
if (errors.length) {
|
||||||
Message.error(`校验未通过:${errors[0].text}`);
|
Message.error(`校验未通过:${errors[0].text}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let content: string;
|
const content = JSON.stringify(toContentJson(model));
|
||||||
if (editMode.value === "canvas") {
|
|
||||||
try {
|
|
||||||
content = serializeUnified();
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(`画布数据读取失败:${e?.message || e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const problems = checkCanvas(content);
|
|
||||||
if (problems.length) {
|
|
||||||
Message.error(problems[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!content || content === "{}") {
|
|
||||||
Message.warning("画布为空,无法保存");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 保存内容同步回卡片树(画布=数据源投影,保存后两侧一致,脏基准重算不误报)
|
|
||||||
try {
|
|
||||||
applyParsed(fromContentJson(JSON.parse(content)));
|
|
||||||
} catch {
|
|
||||||
// 理论不可达(checkCanvas 通过即可反解);失败不阻断保存
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
content = JSON.stringify(toContentJson(model));
|
|
||||||
}
|
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
// code/name 同时出现在 body 与 contentJson 且取值同源,规避 contentJson 静默覆盖行字段的既有坑
|
// code/name 同时出现在 body 与 contentJson 且取值同源,规避 contentJson 静默覆盖行字段的既有坑
|
||||||
@@ -648,98 +373,6 @@ async function onSave(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 画布产物轻校验(保存前拦截;结构完整校验仍以发布期 DefValidator 为准) */
|
|
||||||
function checkCanvas(content: string): string[] {
|
|
||||||
const problems: string[] = [];
|
|
||||||
let def: any;
|
|
||||||
try {
|
|
||||||
def = JSON.parse(content);
|
|
||||||
} catch {
|
|
||||||
return ["画布数据序列化失败,请重试"];
|
|
||||||
}
|
|
||||||
const nodes = def.nodes || [];
|
|
||||||
const edges = def.edges || [];
|
|
||||||
const nodeIds = new Set(nodes.map((n: any) => n.id));
|
|
||||||
if (nodes.filter((n: any) => n.type === "START").length !== 1) {
|
|
||||||
problems.push("画布必须有且仅有 1 个开始节点");
|
|
||||||
}
|
|
||||||
if (nodes.filter((n: any) => n.type === "END").length !== 1) {
|
|
||||||
problems.push("画布必须有且仅有 1 个结束节点");
|
|
||||||
}
|
|
||||||
nodes.forEach((n: any) => {
|
|
||||||
if (n.type === "APPROVAL") {
|
|
||||||
if (!n.name) problems.push("审批节点未命名");
|
|
||||||
if (!n.assignee) problems.push("审批节点「" + (n.name || n.id) + "」缺少审批人配置");
|
|
||||||
}
|
|
||||||
if (n.type === "NOTIFY") {
|
|
||||||
if (!n.name) problems.push("抄送节点未命名");
|
|
||||||
if (!n.receivers) problems.push("抄送节点「" + (n.name || n.id) + "」缺少抄送对象配置");
|
|
||||||
else if (n.receivers.type === "USERS" && !(n.receivers.userIds || []).length) {
|
|
||||||
problems.push("抄送节点「" + (n.name || n.id) + "」未选择用户");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (n.type === "CONDITION") {
|
|
||||||
// 条件分支(镜像后端 DefValidator 的 CONDITION 口径):routerKey 必填 + 出口命名唯一 + 出边 ≥2
|
|
||||||
if (!n.routerKey) problems.push("条件分支「" + (n.name || n.id) + "」未配置判断服务(routerKey)");
|
|
||||||
const outs = edges.filter((e: any) => e.from === n.id);
|
|
||||||
if (outs.length < 2) {
|
|
||||||
problems.push("条件分支「" + (n.name || n.id) + "」至少 2 条出边(当前 " + outs.length + ")");
|
|
||||||
} else {
|
|
||||||
const names = new Set<string>();
|
|
||||||
outs.forEach((e: any) => {
|
|
||||||
const label = e.name?.trim() || "";
|
|
||||||
if (!label) {
|
|
||||||
problems.push(`条件分支「${n.name || n.id}」每个出口必须命名(出口名即判断依据)`);
|
|
||||||
} else if (names.has(label)) {
|
|
||||||
problems.push(`条件分支「${n.name || n.id}」出口名重复:「${label}」`);
|
|
||||||
} else {
|
|
||||||
names.add(label);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
edges.forEach((e: any) => {
|
|
||||||
if (!nodeIds.has(e.to) || !nodeIds.has(e.from)) {
|
|
||||||
problems.push("存在指向缺失节点的连线,请放置后重画");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 多出口命名(镜像后端 DefValidator.validateOutlets 口径)
|
|
||||||
const outNames: Record<string, Set<string>> = {};
|
|
||||||
edges.forEach((e: any) => {
|
|
||||||
(outNames[e.from] = outNames[e.from] || new Set()).add(e.name || "");
|
|
||||||
});
|
|
||||||
nodes.forEach((n: any) => {
|
|
||||||
const outs = edges.filter((e: any) => e.from === n.id);
|
|
||||||
if (n.type === "APPROVAL" && outs.length > 1) {
|
|
||||||
outs.forEach((e: any) => {
|
|
||||||
if (!e.name?.trim()) problems.push(`多出口节点「${n.name || n.id}」每个出口必须命名(按钮文案)`);
|
|
||||||
});
|
|
||||||
outNames[n.id]?.forEach((x) => {
|
|
||||||
// 重复名展示一次即可
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 可达性(simple BFS from start,无孤儿)
|
|
||||||
const start = def.start;
|
|
||||||
if (start) {
|
|
||||||
const seen = new Set([start]);
|
|
||||||
const queue = [start];
|
|
||||||
while (queue.length) {
|
|
||||||
const cur = queue.shift()!;
|
|
||||||
edges.forEach((e: any) => {
|
|
||||||
if (e.from === cur && nodeIds.has(e.to) && !seen.has(e.to)) {
|
|
||||||
seen.add(e.to);
|
|
||||||
queue.push(e.to);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
nodes.forEach((n: any) => {
|
|
||||||
if (!seen.has(n.id)) problems.push(`从开始不可达的节点:${n.name || n.id}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [...new Set(problems)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOpenDiagram(): void {
|
function onOpenDiagram(): void {
|
||||||
if (!editId.value) return;
|
if (!editId.value) return;
|
||||||
@@ -815,10 +448,6 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="hd-right">
|
<div class="hd-right">
|
||||||
<RadioGroup :model-value="editMode" size="small" type="button" @on-change="onModeSwitch">
|
|
||||||
<Radio label="canvas">画布</Radio>
|
|
||||||
<Radio label="cards" :disabled="cardsBroken">卡片</Radio>
|
|
||||||
</RadioGroup>
|
|
||||||
<Button @click="settingsDrawer = true">流程设置</Button>
|
<Button @click="settingsDrawer = true">流程设置</Button>
|
||||||
<Button @click="jsonDrawer = true">源码</Button>
|
<Button @click="jsonDrawer = true">源码</Button>
|
||||||
<Button :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
<Button :disabled="!editId" @click="onOpenDiagram">图示预览</Button>
|
||||||
@@ -826,11 +455,7 @@ onUnmounted(() => window.removeEventListener("beforeunload", onBeforeUnload));
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- 画布模式:bpmn-js 建模(多出口/并联/退回环路均可画) -->
|
<main v-if="!loading" class="ed-canvas">
|
||||||
<BpmnCanvas v-if="!loading && editMode === 'canvas'" ref="bpmnWrap"
|
|
||||||
:source="sourceJson" @node-config="onCanvasNode" />
|
|
||||||
|
|
||||||
<main v-if="!loading && editMode === 'cards'" class="ed-canvas">
|
|
||||||
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
<div class="ed-hint">点击卡片配置节点 · 悬停虚线处 + 插入节点(审批 / 抄送 / 并行分支)</div>
|
||||||
<div class="ed-flow">
|
<div class="ed-flow">
|
||||||
<div class="capsule start">发起开始</div>
|
<div class="capsule start">发起开始</div>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export interface ApprovalNode {
|
|||||||
countersign: Countersign;
|
countersign: Countersign;
|
||||||
/** 办理时限(小时):null=不限,生成 JSON 时省略 */
|
/** 办理时限(小时):null=不限,生成 JSON 时省略 */
|
||||||
deadlineHours: number | null;
|
deadlineHours: number | null;
|
||||||
|
/** 多出口(≥2 才有多出口语义):出口按钮文案,办理人按下哪个按钮走哪条泳道;单出口不写字段 */
|
||||||
|
outlets?: string[];
|
||||||
|
/** 各出口泳道(与 outlets 一一对应);分支尾隐式汇合到后继节点(同条件分支,无 JOIN) */
|
||||||
|
branches?: EditorItem[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 抄送节点(2026-09-23):瞬时知会,不产生待办;receivers 复用审批人结构(DEPT_KEY/USERS) */
|
/** 抄送节点(2026-09-23):瞬时知会,不产生待办;receivers 复用审批人结构(DEPT_KEY/USERS) */
|
||||||
@@ -192,9 +196,12 @@ export interface ContainerRef {
|
|||||||
items: EditorItem[];
|
items: EditorItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 容器型节点(并行/条件分支)的分支数组取用(resolveContainer/removeItem 等共用口径) */
|
/** 容器型节点(并行/条件分支/多出口审批)的分支数组取用(resolveContainer/removeItem 等共用口径) */
|
||||||
export function branchesOf(item: EditorItem | null): EditorItem[][] | null {
|
export function branchesOf(item: EditorItem | null): EditorItem[][] | null {
|
||||||
if (item && (item.kind === "parallel" || item.kind === "condition")) return item.branches;
|
if (item && (item.kind === "parallel" || item.kind === "condition"
|
||||||
|
|| (item.kind === "approval" && item.branches && item.branches.length))) {
|
||||||
|
return item.branches;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +343,8 @@ export interface EditorCtx {
|
|||||||
/** 添加分支(并行/条件分支容器通用;条件分支同步补一个占位出口名) */
|
/** 添加分支(并行/条件分支容器通用;条件分支同步补一个占位出口名) */
|
||||||
addBranch: (containerUid: string) => void;
|
addBranch: (containerUid: string) => void;
|
||||||
removeBranch: (containerUid: string, index: number) => void;
|
removeBranch: (containerUid: string, index: number) => void;
|
||||||
|
/** 退出多出口(多出口审批泳道头部按钮):仅保留出口①泳道内容 */
|
||||||
|
exitOutlets: (uid: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EDITOR_CTX: InjectionKey<EditorCtx> = Symbol("flowDefEditorCtx");
|
export const EDITOR_CTX: InjectionKey<EditorCtx> = Symbol("flowDefEditorCtx");
|
||||||
|
|||||||
@@ -55,6 +55,26 @@ export function validateModel(model: EditorModel, depts: DeptOption[], routers:
|
|||||||
} else if (!a.userIds.length) {
|
} else if (!a.userIds.length) {
|
||||||
push(item.uid, "error", path, "指定用户不能为空");
|
push(item.uid, "error", path, "指定用户不能为空");
|
||||||
}
|
}
|
||||||
|
if (item.outlets && item.outlets.length >= 2) {
|
||||||
|
if (item.outlets.length > MAX_BRANCHES) {
|
||||||
|
push(item.uid, "error", path, `出口数最多 ${MAX_BRANCHES} 个`);
|
||||||
|
}
|
||||||
|
item.outlets.forEach((o, i) => {
|
||||||
|
if (!o.trim()) {
|
||||||
|
push(item.uid, "error", path, `第 ${i + 1} 个出口的按钮文案不能为空`);
|
||||||
|
} else if (item.outlets!.indexOf(o) !== i) {
|
||||||
|
push(item.uid, "error", path, `出口文案重复:「${o}」(按钮文案必须唯一)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(item.branches || []).forEach((branch, i) => {
|
||||||
|
const p = `${path}·出口「${item.outlets![i] || i + 1}」`;
|
||||||
|
if (!branch.length) {
|
||||||
|
push(item.uid, "error", p, "出口泳道为空,请添加节点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rec(branch, p);
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,13 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
|
|||||||
target: "http://localhost:8085",
|
target: "http://localhost:8085",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
// 免签面(嵌入页数据 /open/flow/embed/data、示意图 /open/flow/diagram/img):
|
||||||
|
// 生产前后端同域(Caddy /flow-center 下同源相对路径即达),本地 dev 前后端分端口,
|
||||||
|
// 同理需代理——serviceHost 为空时浏览器请求落在 5173/8088 上无此路由
|
||||||
|
"/open": {
|
||||||
|
target: "http://localhost:8085",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// css预处理器
|
// css预处理器
|
||||||
|
|||||||
Reference in New Issue
Block a user