chore(flow-editor)!: 移除 bpmn-js 依赖与画布文件

This commit is contained in:
chenxing
2026-10-03 15:13:35 +08:00
parent ffa196954e
commit 138cb36800
5 changed files with 0 additions and 837 deletions
-223
View File
@@ -15,7 +15,6 @@
"@wangeditor/editor-for-vue": "^5.1.12",
"alova": "^3.4.0",
"axios": "^0.24.0",
"bpmn-js": "^17.11.1",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
@@ -296,16 +295,6 @@
"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": {
"version": "7.1.2",
"resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
@@ -2974,36 +2963,6 @@
"dev": true,
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
@@ -3306,15 +3265,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -3373,12 +3323,6 @@
"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": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
@@ -4325,79 +4269,6 @@
"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": {
"version": "1.0.1",
"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"
}
},
"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": {
"version": "3.4.15",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.15.tgz",
@@ -5660,12 +5522,6 @@
"integrity": "sha512-CW/pPvSOFIRsosbwxrYaE9ERmpTo5fbTqL7wCvuCFlqW1Bmb1K5fXsY7yiH95rmlr4rVuA7UXHbM/cIXQ6AXwg==",
"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": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -5851,12 +5707,6 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
@@ -5946,12 +5796,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -7153,23 +6997,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": {
"version": "1.2.8",
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
@@ -7236,26 +7063,6 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
@@ -7624,15 +7431,6 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/object-visit/-/object-visit-1.0.1.tgz",
@@ -7929,15 +7727,6 @@
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -8860,12 +8649,6 @@
"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": {
"version": "2.2.31",
"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==",
"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": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
-1
View File
@@ -22,7 +22,6 @@
"@wangeditor/editor-for-vue": "^5.1.12",
"alova": "^3.4.0",
"axios": "^0.24.0",
"bpmn-js": "^17.11.1",
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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>`;
}
@@ -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>