chore(flow-editor)!: 移除 bpmn-js 依赖与画布文件
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user